@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
@@ -67,7 +67,7 @@ Behavior:
67
67
 
68
68
  ## Deferred tool groups
69
69
 
70
- Extensions can register tools with Tau's `load_tools` registry while keeping their schemas out of the active tool set until the agent needs them. The extension and Tau must run in the same Pi runtime.
70
+ Extensions can register tools as deferred tools, keeping their schemas out of the active tool set until the agent loads them with Pi's built-in `tool_search`. Tau keeps `tool_search` active whenever deferred tools exist.
71
71
 
72
72
  ```ts
73
73
  import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -96,9 +96,9 @@ export default function confluenceExtension(pi: ExtensionAPI): void {
96
96
  }
97
97
  ```
98
98
 
99
- `registerDeferredToolGroup()` registers tool definitions with Pi and exposes the group through `load_tools`. Call it during extension initialization. Project-local and global package extensions use the same API. `id` must be unique in the runtime, and tool names must be unique within the group.
99
+ `registerDeferredToolGroup()` registers each tool with `exposure: "deferred"` and a namespace made from `id` and `description`. Call it during extension initialization. Project-local and global package extensions use the same API. Tool names must be unique within the group.
100
100
 
101
- Deferred loading affects model-visible tool schemas, not JavaScript package loading. Initialize expensive clients, authentication, and network connections inside tool execution when possible. Pi handles provider-specific deferred-tool behavior after Tau additively activates the group.
101
+ Deferred loading affects model-visible tool schemas, not JavaScript package loading. Initialize expensive clients, authentication, and network connections inside tool execution when possible. Pi handles provider-specific deferred-tool behavior when `tool_search` loads a tool. On models that cannot take tool changes without replacing the cached prefix, Tau blocks `tool_search`.
102
102
 
103
103
  ## Events
104
104
 
@@ -109,6 +109,7 @@ function registerAppshotTools(pi: ExtensionAPI, runHelper: RunHelper): void {
109
109
  const listWindowsTool = defineTool<typeof listWindowsSchema, undefined>({
110
110
  name: "list_windows",
111
111
  label: "List Windows",
112
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
112
113
  description:
113
114
  "List visible normal macOS windows as compact JSON with window IDs, titles, application identity, process IDs, and bounds. Use list_windows to discover exact window IDs and application PIDs before screenshot_window or activate_app. Requires macOS 14 or newer and Screen & System Audio Recording permission.",
114
115
  parameters: listWindowsSchema,
@@ -145,6 +146,7 @@ function registerAppshotTools(pi: ExtensionAPI, runHelper: RunHelper): void {
145
146
  const screenshotWindowTool = defineTool<typeof screenshotWindowSchema, ScreenshotDetails | undefined>({
146
147
  name: "screenshot_window",
147
148
  label: "Screenshot Window",
149
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
148
150
  description:
149
151
  "Capture one visible macOS window by an exact ID returned by list_windows, resize it to fit within 1568×1568 pixels, save it to the required PNG path, and inspect the image. Call list_windows first.",
150
152
  parameters: screenshotWindowSchema,
@@ -202,6 +204,7 @@ function registerAppshotTools(pi: ExtensionAPI, runHelper: RunHelper): void {
202
204
  const activateAppTool = defineTool<typeof activateAppSchema, ActivationDetails>({
203
205
  name: "activate_app",
204
206
  label: "Activate App",
207
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
205
208
  description:
206
209
  "Bring a running macOS application and its windows to the foreground by a process ID returned by list_windows. Use only when foregrounding is required for visual validation because activate_app changes user focus.",
207
210
  parameters: activateAppSchema,
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { normalizeContext, type Context, type Message, type Model } from "@earendil-works/pi-ai";
2
+ import type { Context, Message, Model } from "@earendil-works/pi-ai";
3
3
  import {
4
4
  buildSessionContext,
5
5
  convertToLlm,
@@ -128,11 +128,6 @@ async function runAside(
128
128
  withConversation: boolean,
129
129
  signal: AbortSignal,
130
130
  ): Promise<AsideResult> {
131
- const provider = ctx.modelRegistry.getProvider(model.provider);
132
- if (!provider) throw new Error(`Provider ${model.provider} is unavailable`);
133
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
134
- if (!auth.ok) throw new Error(auth.error);
135
-
136
131
  const request = withConversation
137
132
  ? buildAsideRequest(
138
133
  convertToLlm(buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages),
@@ -140,11 +135,8 @@ async function runAside(
140
135
  ctx.getSystemPrompt(),
141
136
  )
142
137
  : buildAsideRequest([], question, undefined);
143
- const response = await provider
144
- .streamSimple(model, normalizeContext(request), {
145
- apiKey: auth.apiKey,
146
- headers: auth.headers,
147
- env: auth.env,
138
+ const response = await ctx.modelRegistry
139
+ .streamSimple(model, request, {
148
140
  signal,
149
141
  reasoning: ctx.thinkingLevel === "off" ? undefined : ctx.thinkingLevel,
150
142
  sessionId: withConversation ? ctx.sessionManager.getSessionId() : randomUUID(),
@@ -4,10 +4,8 @@ Sends a terminal-driven attention notification when Tau is ready for input, fini
4
4
 
5
5
  ## Behavior
6
6
 
7
- - Emits an attention notification after the agent settles with no automatic continuation pending.
8
- - Waits for automatic post-turn checks before deciding whether the agent is ready for input.
9
- - Emits an attention notification on `session_compact` unless another extension has an active attention hold.
10
- - Defers settlement and compaction notifications while an attention hold is active.
7
+ - Emits an attention notification after the agent settles with no automatic continuation pending. Automatic post-turn checks run before the run settles, so this notification already waits for them.
8
+ - Emits an attention notification on `session_compact` when the agent is idle. A compaction in the middle of a run is followed by the settle notification instead.
11
9
  - Emits an attention notification on `session_tree` when it includes a branch summary.
12
10
  - Listens for shared event `tau:agent.blocked` when Tau is waiting on user input.
13
11
  - Uses the terminal or host OS notification path that best fits the current environment.
@@ -25,10 +25,6 @@ function playMacOSSound(pi: ExtensionAPI): void {
25
25
  }
26
26
 
27
27
  export default function attentionExtension(pi: ExtensionAPI): void {
28
- const holds = new Set<string>();
29
- let pendingAttention: (() => void) | undefined;
30
- let discardNextSettlement = false;
31
-
32
28
  function notify(data: { title?: string; body?: string }): void {
33
29
  const raw: unknown = data;
34
30
  const record = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
@@ -69,42 +65,14 @@ export default function attentionExtension(pi: ExtensionAPI): void {
69
65
  }
70
66
 
71
67
  onTauEvent(pi, "attention.agent-blocked", "tau:agent.blocked", notify);
72
- onTauEvent(pi, "attention.hold-acquire", "tau:attention.hold.acquire", ({ id }) => {
73
- holds.add(id);
74
- });
75
- onTauEvent(pi, "attention.hold-release", "tau:attention.hold.release", ({ id, disposition }) => {
76
- if (!holds.delete(id)) return;
77
- if (disposition === "discard") {
78
- if (pendingAttention) pendingAttention = undefined;
79
- else discardNextSettlement = true;
80
- }
81
- if (holds.size > 0 || !pendingAttention) return;
82
- const notifyPending = pendingAttention;
83
- pendingAttention = undefined;
84
- notifyPending();
85
- });
86
-
87
- pi.on("session_start", () => {
88
- holds.clear();
89
- pendingAttention = undefined;
90
- discardNextSettlement = false;
91
- });
92
-
93
68
  pi.on("agent_settled", (_event, ctx) => {
94
69
  if (ctx.mode === "print") return;
95
- if (discardNextSettlement) {
96
- discardNextSettlement = false;
97
- return;
98
- }
99
- if (holds.size > 0) {
100
- pendingAttention = () => notify({ title: DEFAULT_TITLE, body: DEFAULT_BODY });
101
- return;
102
- }
103
70
  notify({ title: DEFAULT_TITLE, body: DEFAULT_BODY });
104
71
  });
105
72
 
106
73
  pi.on("session_compact", (_event, ctx) => {
107
- if (ctx.mode === "print" || holds.size > 0) return;
74
+ // A compaction in the middle of a run is followed by the settle notification.
75
+ if (ctx.mode === "print" || !ctx.isIdle()) return;
108
76
  notify({ title: DEFAULT_TITLE, body: COMPACTION_BODY });
109
77
  });
110
78
 
@@ -112,10 +80,4 @@ export default function attentionExtension(pi: ExtensionAPI): void {
112
80
  if (ctx.mode === "print" || !event.summaryEntry) return;
113
81
  notify({ title: DEFAULT_TITLE, body: BRANCH_SUMMARY_BODY });
114
82
  });
115
-
116
- pi.on("session_shutdown", () => {
117
- holds.clear();
118
- pendingAttention = undefined;
119
- discardNextSettlement = false;
120
- });
121
83
  }
@@ -1,21 +1,18 @@
1
- import { type ThinkingLevel, type Tool, Type } from "@earendil-works/pi-ai";
1
+ import { type Tool, Type } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  getBranchInjectedContexts,
5
5
  getPendingInjectedContexts,
6
6
  type InjectedContext,
7
7
  } from "../../shared/injected-context.ts";
8
- import { generateToolValidated, resolveCandidates } from "../../shared/model-fallback/index.ts";
8
+ import { generateToolValidated } from "../../shared/model-fallback/index.ts";
9
+ import { resolveEffortCandidates } from "../../shared/model-effort.ts";
9
10
  import { errorText, truncAt } from "../../shared/text.ts";
10
11
 
11
12
  const STATUS_KEY = "auto-name";
12
13
  const SENTINEL = "NONE";
13
14
  const MAX_NAME_LENGTH = 80;
14
15
  const MAX_NAMING_INPUT_CHARS = 120_000;
15
- const AUTO_NAME_MODELS: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }> = [
16
- { provider: "openai-codex", model: "gpt-5.4-mini", reasoning: "medium" },
17
- { provider: "openrouter", model: "cohere/north-mini-code:free", reasoning: "high" },
18
- ];
19
16
 
20
17
  const NAMING_PROMPT = [
21
18
  "You are naming a chat session based on the user's first message and any injected hidden context.",
@@ -87,9 +84,9 @@ async function runAutoName(
87
84
  ): Promise<void> {
88
85
  const ui = ctx.ui;
89
86
  try {
90
- const candidates = await resolveCandidates(ctx, AUTO_NAME_MODELS, true);
87
+ const candidates = await resolveEffortCandidates(ctx, "quick", { includeParentModel: true });
91
88
  const { value: result } = await generateToolValidated(
92
- { ui, signal: controller.signal },
89
+ { ui, modelRegistry: ctx.modelRegistry, signal: controller.signal },
93
90
  candidates,
94
91
  `${NAMING_PROMPT}\n\n${prompt}`,
95
92
  NAME_SESSION_TOOL,
@@ -487,7 +487,7 @@ export default function cacheDiagnosticsExtension(pi: ExtensionAPI): void {
487
487
  );
488
488
  pi.on("session_tree", () => addMarker("session-tree"));
489
489
  pi.on("tool_execution_end", (event) => {
490
- if (event.toolName !== "load_tools") return;
490
+ if (event.toolName !== "tool_search") return;
491
491
  return addMarker("cache-affecting-tool", { tool: event.toolName, isError: event.isError });
492
492
  });
493
493
  }
@@ -0,0 +1,7 @@
1
+ # Codex Priority
2
+
3
+ Requests Codex priority processing (Fast mode) for every `gpt-6-luna` request on the `openai-codex` provider: the agent loop, `/compact` summaries, tool approval, auto-naming, commit, and handoff. Priority gives faster responses at a higher credit rate, so the approval reviewer and compaction block you for less time.
4
+
5
+ There is no command and no setting. Other models are unchanged. Displayed cost uses OpenAI's documented 2.5x Fast mode rate for GPT-6 models.
6
+
7
+ Do not run another extension that overlays the `openai-codex` provider or sets `service_tier`; the one that loads last wins.
@@ -0,0 +1,95 @@
1
+ import {
2
+ calculateCost,
3
+ clampThinkingLevel,
4
+ createAssistantMessageEventStream,
5
+ type Api,
6
+ type AssistantMessage,
7
+ type AssistantMessageEventStream,
8
+ type Model,
9
+ type ModelThinkingLevel,
10
+ type SimpleStreamOptions,
11
+ type Usage,
12
+ } from "@earendil-works/pi-ai";
13
+ import { openAICodexResponsesApi } from "@earendil-works/pi-ai/compat";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+
16
+ const PRIORITY_MODEL_ID = "gpt-6-luna";
17
+ // OpenAI documents Codex Fast mode as 2.5x credits for GPT-6 models. pi-ai applies 2x, so cost is recomputed.
18
+ const PRIORITY_MULTIPLIER = 2.5;
19
+
20
+ type CodexOptions = SimpleStreamOptions & {
21
+ serviceTier?: "priority";
22
+ reasoningEffort?: Exclude<ModelThinkingLevel, "off">;
23
+ };
24
+
25
+ function reprice(model: Model<Api>, usage: Usage): void {
26
+ calculateCost(model, usage);
27
+ usage.cost.input *= PRIORITY_MULTIPLIER;
28
+ usage.cost.output *= PRIORITY_MULTIPLIER;
29
+ usage.cost.cacheRead *= PRIORITY_MULTIPLIER;
30
+ usage.cost.cacheWrite *= PRIORITY_MULTIPLIER;
31
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
32
+ }
33
+
34
+ function errorMessage(model: Model<Api>, error: unknown): AssistantMessage {
35
+ return {
36
+ role: "assistant",
37
+ content: [],
38
+ api: model.api,
39
+ provider: model.provider,
40
+ model: model.id,
41
+ usage: {
42
+ input: 0,
43
+ output: 0,
44
+ cacheRead: 0,
45
+ cacheWrite: 0,
46
+ totalTokens: 0,
47
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
48
+ },
49
+ stopReason: "error",
50
+ errorMessage: error instanceof Error ? error.message : String(error),
51
+ timestamp: Date.now(),
52
+ };
53
+ }
54
+
55
+ function withPriorityPricing(source: AssistantMessageEventStream, model: Model<Api>): AssistantMessageEventStream {
56
+ const output = createAssistantMessageEventStream();
57
+ void (async () => {
58
+ try {
59
+ for await (const event of source) {
60
+ if (event.type === "done") reprice(model, event.message.usage);
61
+ else if (event.type === "error") reprice(model, event.error.usage);
62
+ output.push(event);
63
+ }
64
+ const result = await source.result();
65
+ reprice(model, result.usage);
66
+ output.end(result);
67
+ } catch (error) {
68
+ const message = errorMessage(model, error);
69
+ output.push({ type: "error", reason: "error", error: message });
70
+ output.end(message);
71
+ }
72
+ })();
73
+ return output;
74
+ }
75
+
76
+ export default function codexPriorityExtension(pi: ExtensionAPI): void {
77
+ const codex = openAICodexResponsesApi();
78
+ // Overlay the built-in provider so its models and authentication stay intact. Pi routes both
79
+ // stream() and streamSimple() for this API here, including the agent loop and registry calls.
80
+ pi.registerProvider("openai-codex", {
81
+ api: "openai-codex-responses",
82
+ streamSimple(model, context, options) {
83
+ const priority = model.id === PRIORITY_MODEL_ID;
84
+ const request: CodexOptions = { ...options };
85
+ // A stream() caller passes reasoningEffort itself; only derive it from the neutral option.
86
+ if (options?.reasoning !== undefined) {
87
+ const level = clampThinkingLevel(model, options.reasoning);
88
+ request.reasoningEffort = level === "off" ? undefined : level;
89
+ }
90
+ if (priority) request.serviceTier = "priority";
91
+ const stream = codex.stream(model, context, request);
92
+ return priority ? withPriorityPricing(stream, model) : stream;
93
+ },
94
+ });
95
+ }
@@ -0,0 +1,5 @@
1
+ # Compaction
2
+
3
+ Writes compaction summaries with a cheaper model from the same provider as the session, so long Opus, GPT-6.1 Sol, and Astra sessions do not pay their own rates to summarize themselves.
4
+
5
+ Compaction, `/compact`, and overflow recovery use the `quick` effort tier for the session's provider (for example `gpt-6-luna` for OpenAI Codex and `claude-sonnet-5-5` for Anthropic). Summaries never cross providers. A notice names the model that wrote each summary, and the saved compaction entry is marked `fromHook: true`. File lists from earlier compactions are carried forward. When the session model is already the quick model, the provider has no quick model, the quick model's context window is too small for the conversation, or every quick model fails, Pi's default compaction runs with the session model.
@@ -0,0 +1,47 @@
1
+ import { compact, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { resolveEffortCandidates } from "../../shared/model-effort.ts";
3
+ import { errorText } from "../../shared/text.ts";
4
+
5
+ export default function compactionExtension(pi: ExtensionAPI): void {
6
+ pi.on("session_before_compact", async (event, ctx) => {
7
+ const session = ctx.model;
8
+ if (!session) return undefined;
9
+ const { preparation } = event;
10
+ // Summaries stay on the session's provider, and the cheaper model needs room for the whole input.
11
+ const candidates = (await resolveEffortCandidates(ctx, "quick", { includeParentModel: false })).filter(
12
+ ({ model }) =>
13
+ model.provider === session.provider &&
14
+ model.id !== session.id &&
15
+ model.contextWindow >= preparation.tokensBefore + preparation.settings.reserveTokens,
16
+ );
17
+ for (const { model, reasoning } of candidates) {
18
+ try {
19
+ // Pi only carries file lists forward from its own compactions, not from extension-made ones.
20
+ const previous = [...event.branchEntries].reverse().find((entry) => entry.type === "compaction");
21
+ const details = previous?.type === "compaction" && previous.fromHook ? previous.details : undefined;
22
+ if (details && typeof details === "object") {
23
+ const { readFiles, modifiedFiles } = details as { readFiles?: unknown; modifiedFiles?: unknown };
24
+ if (Array.isArray(readFiles)) for (const file of readFiles) preparation.fileOps.read.add(String(file));
25
+ if (Array.isArray(modifiedFiles))
26
+ for (const file of modifiedFiles) preparation.fileOps.edited.add(String(file));
27
+ }
28
+ const result = await compact(
29
+ preparation,
30
+ model,
31
+ undefined,
32
+ undefined,
33
+ event.customInstructions,
34
+ event.signal,
35
+ reasoning,
36
+ (streamModel, context, options) => ctx.modelRegistry.streamSimple(streamModel, context, options),
37
+ );
38
+ ctx.ui.notify(`Compaction summarized with ${model.provider}/${model.id}`, "info");
39
+ return { compaction: result };
40
+ } catch (error) {
41
+ if (event.signal.aborted) return undefined;
42
+ ctx.ui.notify(`Compaction with ${model.provider}/${model.id} failed: ${errorText(error)}`, "warning");
43
+ }
44
+ }
45
+ return undefined;
46
+ });
47
+ }
@@ -29,4 +29,4 @@ All-sessions reports include a project ranking and a project filter on the sessi
29
29
 
30
30
  Reports are written under `~/.pi/tau/cost-reports/` and opened automatically. Empty windows get a warning instead of a file.
31
31
 
32
- Costs come from stored session usage estimates, including subagent tool results on parent sessions.
32
+ Costs come from stored session usage estimates, including model calls made by tools.
@@ -11,7 +11,6 @@ import type {
11
11
  ReportScope,
12
12
  SessionCost,
13
13
  SessionModelCost,
14
- SubagentCost,
15
14
  } from "./types.ts";
16
15
 
17
16
  export interface BuildCostReportOptions {
@@ -31,12 +30,6 @@ interface ModelBucket {
31
30
  sessions: Set<string>;
32
31
  }
33
32
 
34
- interface SubagentBucket {
35
- agent: string;
36
- cost: number;
37
- calls: number;
38
- }
39
-
40
33
  interface ProjectBucket {
41
34
  key: string;
42
35
  label: string;
@@ -53,11 +46,9 @@ interface SessionModelBucket {
53
46
 
54
47
  interface AnalyzedSession {
55
48
  session: SessionCost;
56
- directCost: number;
57
- subagentCost: number;
49
+ cost: number;
58
50
  dayCosts: Map<string, number>;
59
51
  models: ModelBucket[];
60
- subagents: SubagentBucket[];
61
52
  }
62
53
 
63
54
  function numberOrZero(value: unknown): number {
@@ -167,15 +158,12 @@ export async function buildCostReport(options: BuildCostReportOptions): Promise<
167
158
 
168
159
  const dayMap = new Map<string, number>();
169
160
  const modelMap = new Map<string, ModelBucket>();
170
- const subagentMap = new Map<string, SubagentBucket>();
171
161
  const projectMap = new Map<string, ProjectBucket>();
172
- let directCost = 0;
173
- let subagentCost = 0;
162
+ let totalCost = 0;
174
163
  let totalTokens = 0;
175
164
 
176
165
  for (const item of analyzed) {
177
- directCost += item.directCost;
178
- subagentCost += item.subagentCost;
166
+ totalCost += item.cost;
179
167
  totalTokens += item.session.tokens;
180
168
 
181
169
  for (const [key, cost] of item.dayCosts) {
@@ -196,16 +184,6 @@ export async function buildCostReport(options: BuildCostReportOptions): Promise<
196
184
  }
197
185
  }
198
186
 
199
- for (const agent of item.subagents) {
200
- const existing = subagentMap.get(agent.agent);
201
- if (existing) {
202
- existing.cost += agent.cost;
203
- existing.calls += agent.calls;
204
- } else {
205
- subagentMap.set(agent.agent, { ...agent });
206
- }
207
- }
208
-
209
187
  const project = projectMap.get(item.session.projectKey);
210
188
  if (project) {
211
189
  project.cost += item.session.cost;
@@ -234,8 +212,6 @@ export async function buildCostReport(options: BuildCostReportOptions): Promise<
234
212
  }))
235
213
  .sort((a, b) => b.tokens - a.tokens || b.cost - a.cost);
236
214
 
237
- const subagents: SubagentCost[] = [...subagentMap.values()].sort((a, b) => b.cost - a.cost);
238
-
239
215
  const projects: ProjectCost[] = [...projectMap.values()]
240
216
  .map((bucket) => ({
241
217
  key: bucket.key,
@@ -253,15 +229,12 @@ export async function buildCostReport(options: BuildCostReportOptions): Promise<
253
229
  cwd,
254
230
  scope,
255
231
  range,
256
- directCost,
257
- subagentCost,
258
- totalCost: directCost + subagentCost,
232
+ totalCost,
259
233
  totalTokens,
260
234
  sessionCount: sessions.length,
261
235
  projectCount: projects.length,
262
236
  days,
263
237
  models,
264
- subagents,
265
238
  projects,
266
239
  sessions,
267
240
  };
@@ -293,9 +266,7 @@ function analyzeSession(info: SessionInfo, range: ReportRange): AnalyzedSession
293
266
 
294
267
  const dayCosts = new Map<string, number>();
295
268
  const modelBuckets = new Map<string, SessionModelBucket & { tokens: number; provider: string; model: string }>();
296
- const subagentBuckets = new Map<string, SubagentBucket>();
297
- let directCost = 0;
298
- let subagentCost = 0;
269
+ let cost = 0;
299
270
  let tokens = 0;
300
271
  let startedAtMs = Number.POSITIVE_INFINITY;
301
272
 
@@ -306,7 +277,7 @@ function analyzeSession(info: SessionInfo, range: ReportRange): AnalyzedSession
306
277
  if (ts === undefined || !inRange(ts, range)) continue;
307
278
  const usage = normalizeUsage(entry.usage);
308
279
  if (usage.cost <= 0 && usage.totalTokens <= 0) continue;
309
- directCost += usage.cost;
280
+ cost += usage.cost;
310
281
  tokens += usage.totalTokens;
311
282
  dayCosts.set(localDateKey(ts), (dayCosts.get(localDateKey(ts)) ?? 0) + usage.cost);
312
283
  if (ts < startedAtMs) startedAtMs = ts;
@@ -330,7 +301,7 @@ function analyzeSession(info: SessionInfo, range: ReportRange): AnalyzedSession
330
301
  if (message.role === "assistant" && message.usage) {
331
302
  const usage = normalizeUsage(message.usage);
332
303
  if (usage.cost <= 0 && usage.totalTokens <= 0) continue;
333
- directCost += usage.cost;
304
+ cost += usage.cost;
334
305
  tokens += usage.totalTokens;
335
306
  dayCosts.set(localDateKey(ts), (dayCosts.get(localDateKey(ts)) ?? 0) + usage.cost);
336
307
  if (ts < startedAtMs) startedAtMs = ts;
@@ -361,29 +332,11 @@ function analyzeSession(info: SessionInfo, range: ReportRange): AnalyzedSession
361
332
  dayCosts.set(localDateKey(ts), (dayCosts.get(localDateKey(ts)) ?? 0) + usage.cost);
362
333
  if (ts < startedAtMs) startedAtMs = ts;
363
334
  tokens += usage.totalTokens;
364
-
365
- if (message.toolName === "subagent") {
366
- subagentCost += usage.cost;
367
- const details = asRecord(message.details);
368
- const agent =
369
- (typeof details?.agent === "string" && details.agent) ||
370
- (typeof details?.displayName === "string" && details.displayName) ||
371
- "subagent";
372
- const existing = subagentBuckets.get(agent);
373
- if (existing) {
374
- existing.cost += usage.cost;
375
- existing.calls += 1;
376
- } else {
377
- subagentBuckets.set(agent, { agent, cost: usage.cost, calls: 1 });
378
- }
379
- } else {
380
- directCost += usage.cost;
381
- }
335
+ cost += usage.cost;
382
336
  }
383
337
  }
384
338
 
385
- const sessionCost = directCost + subagentCost;
386
- if (sessionCost <= 0 && tokens <= 0) return undefined;
339
+ if (cost <= 0 && tokens <= 0) return undefined;
387
340
 
388
341
  const models: ModelBucket[] = [...modelBuckets.values()].map((bucket) => ({
389
342
  key: bucket.key,
@@ -410,14 +363,12 @@ function analyzeSession(info: SessionInfo, range: ReportRange): AnalyzedSession
410
363
  projectKey: project.key,
411
364
  projectLabel: project.label,
412
365
  startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : range.startMs,
413
- cost: sessionCost,
366
+ cost,
414
367
  tokens,
415
368
  models: sessionModels,
416
369
  },
417
- directCost,
418
- subagentCost,
370
+ cost,
419
371
  dayCosts,
420
372
  models,
421
- subagents: [...subagentBuckets.values()],
422
373
  };
423
374
  }
@@ -126,7 +126,6 @@ export function renderCostReportHtml(report: CostReport): string {
126
126
  const scopeClass = report.scope === "project" ? "scope-project" : "scope-all";
127
127
  const totalShare = report.totalCost;
128
128
  const tokenShareBase = report.totalTokens;
129
- const subagentShareBase = report.subagentCost;
130
129
 
131
130
  const modelRows = report.models
132
131
  .map((model) => {
@@ -142,22 +141,6 @@ export function renderCostReportHtml(report: CostReport): string {
142
141
  })
143
142
  .join("");
144
143
 
145
- const subagentRows =
146
- report.subagents.length === 0
147
- ? `<tr><td class="px-3 py-2 text-stone-600 dark:text-stone-400" colspan="5">No subagent spend in this window.</td></tr>`
148
- : report.subagents
149
- .map((agent) => {
150
- const share = subagentShareBase > 0 ? agent.cost / subagentShareBase : 0;
151
- return `<tr>
152
- <td class="px-3 py-2">${escapeHtml(agent.agent)}</td>
153
- <td class="px-3 py-2 text-right">${money(agent.cost)}</td>
154
- <td class="px-3 py-2 text-right">${percent(agent.cost, subagentShareBase)}</td>
155
- <td class="px-3 py-2">${shareBar(share)}</td>
156
- <td class="px-3 py-2 text-right">${agent.calls}</td>
157
- </tr>`;
158
- })
159
- .join("");
160
-
161
144
  const projectRows = report.projects
162
145
  .map((project) => {
163
146
  const share = totalShare > 0 ? project.cost / totalShare : 0;
@@ -386,24 +369,6 @@ export function renderCostReportHtml(report: CostReport): string {
386
369
  </table>
387
370
  </div>
388
371
  </section>
389
-
390
- <section aria-labelledby="subagents-heading">
391
- <h2 id="subagents-heading" class="mb-3 text-sm font-semibold">Subagents</h2>
392
- <div class="overflow-x-auto rounded border border-stone-300 bg-white dark:border-stone-700 dark:bg-stone-900">
393
- <table class="w-full text-sm tabular">
394
- <thead>
395
- <tr class="text-left text-xs font-medium text-stone-600 dark:text-stone-400">
396
- <th scope="col" class="px-3 py-2 font-medium">Agent</th>
397
- <th scope="col" class="px-3 py-2 text-right font-medium">Cost</th>
398
- <th scope="col" class="px-3 py-2 text-right font-medium">Share</th>
399
- <th scope="col" class="min-w-[7rem] px-3 py-2 font-medium"></th>
400
- <th scope="col" class="px-3 py-2 text-right font-medium">Calls</th>
401
- </tr>
402
- </thead>
403
- <tbody class="divide-y divide-stone-200 dark:divide-stone-800">${subagentRows}</tbody>
404
- </table>
405
- </div>
406
- </section>
407
372
  </div>
408
373
 
409
374
  <section class="all-only mb-8" aria-labelledby="projects-heading">
@@ -450,8 +415,7 @@ export function renderCostReportHtml(report: CostReport): string {
450
415
 
451
416
  <footer class="mt-10 border-t border-stone-300 pt-4 text-xs text-stone-600 dark:border-stone-700 dark:text-stone-400">
452
417
  <p>
453
- Costs are estimates from stored session usage, not provider invoices. Branched turns count. Subagent
454
- totals come from parent tool results. Direct ${money(report.directCost)} · Subagents ${money(report.subagentCost)}.
418
+ Costs are estimates from stored session usage, not provider invoices. Branched turns count.
455
419
  </p>
456
420
  </footer>
457
421
  </main>
@@ -34,12 +34,6 @@ export interface ModelCost {
34
34
  sessions: number;
35
35
  }
36
36
 
37
- export interface SubagentCost {
38
- agent: string;
39
- cost: number;
40
- calls: number;
41
- }
42
-
43
37
  export interface ProjectCost {
44
38
  key: string;
45
39
  label: string;
@@ -79,15 +73,12 @@ export interface CostReport {
79
73
  cwd: string;
80
74
  scope: ReportScope;
81
75
  range: ReportRange;
82
- directCost: number;
83
- subagentCost: number;
84
76
  totalCost: number;
85
77
  totalTokens: number;
86
78
  sessionCount: number;
87
79
  projectCount: number;
88
80
  days: DayCost[];
89
81
  models: ModelCost[];
90
- subagents: SubagentCost[];
91
82
  projects: ProjectCost[];
92
83
  sessions: SessionCost[];
93
84
  }
@@ -61,7 +61,7 @@ export default function handoffExtension(pi: ExtensionAPI): void {
61
61
  );
62
62
  const conversation = serializeConversation(convertToLlm(messages));
63
63
  const result = await generateToolValidated(
64
- { ui: ctx.ui, signal: loader.signal },
64
+ { ui: ctx.ui, modelRegistry: ctx.modelRegistry, signal: loader.signal },
65
65
  candidates,
66
66
  buildHandoffRequest(conversation, goal, ctx.cwd),
67
67
  HANDOFF_TOOL,
@@ -40,6 +40,7 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
40
40
  defineTool<typeof imageGenSchema, ImageGenDetails | undefined>({
41
41
  name: "image_gen",
42
42
  label: "Image Generation",
43
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
43
44
  description:
44
45
  "Generate a requested raster image or AI-edit existing images with OpenAI GPT Image or xAI Grok Imagine. Omit provider to follow the parent model; set it to openai or xai to override. Omit referenced_image_paths to generate; pass one to three local paths to edit or compose. Omit path to use Tau's external image store; pass path only when the user explicitly requests a repository file or other destination. Returns the image for inspection.",
45
46
  parameters: imageGenSchema,