@shanepadgett/tau-agent 0.29.0 → 0.30.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.
Files changed (44) hide show
  1. package/docs/context.md +14 -9
  2. package/docs/subagents.md +7 -5
  3. package/extensions/cache-diagnostics/index.ts +3 -4
  4. package/extensions/context/README.md +8 -5
  5. package/extensions/context/definitions.ts +28 -17
  6. package/extensions/context/evidence.ts +4 -3
  7. package/extensions/context/index.ts +57 -35
  8. package/extensions/context/panel.ts +10 -9
  9. package/extensions/context/projection.ts +141 -0
  10. package/extensions/context/state.ts +30 -0
  11. package/extensions/explore/ast/read/hook.ts +3 -1
  12. package/extensions/ideas/browser.ts +27 -16
  13. package/extensions/review/README.md +11 -0
  14. package/extensions/review/index.ts +135 -0
  15. package/extensions/review/model.ts +144 -0
  16. package/extensions/review/panel.ts +128 -0
  17. package/extensions/review/session.ts +106 -0
  18. package/extensions/script-runner/README.md +7 -0
  19. package/extensions/script-runner/index.ts +275 -0
  20. package/extensions/stash/browser.ts +30 -18
  21. package/extensions/subagent/README.md +17 -4
  22. package/extensions/subagent/agents/context-sync.md +2 -2
  23. package/extensions/subagent/agents/scout.md +48 -61
  24. package/extensions/subagent/agents.ts +0 -1
  25. package/extensions/subagent/cmux-dashboard.ts +7 -3
  26. package/extensions/subagent/index.ts +105 -4
  27. package/extensions/subagent/panel.ts +124 -0
  28. package/extensions/subagent/render.ts +2 -1
  29. package/extensions/subagent/run.ts +9 -9
  30. package/extensions/subagent/runtime.ts +5 -5
  31. package/extensions/subagent/session-resource.ts +19 -127
  32. package/extensions/subagent/settings.ts +18 -0
  33. package/extensions/tau-help/help.md +10 -6
  34. package/extensions/working-memory/index.ts +21 -3
  35. package/extensions/working-memory/memory.ts +23 -5
  36. package/package.json +2 -2
  37. package/schemas/tau.schema.json +9 -23
  38. package/shared/context-messages.ts +19 -0
  39. package/shared/injected-context.ts +2 -2
  40. package/shared/isolated-session.ts +151 -0
  41. package/extensions/subagent/agents/review.md +0 -75
  42. package/extensions/turn-budget/README.md +0 -14
  43. package/extensions/turn-budget/index.ts +0 -116
  44. package/extensions/turn-budget/settings.ts +0 -35
@@ -6,9 +6,17 @@ import { createToolRowStateStore } from "../../shared/tool-row-state.js";
6
6
  import contextSettings from "../context/settings.ts";
7
7
  import { discoverAgents, type AgentDiscovery } from "./agents.ts";
8
8
  import { createCmuxDashboard, type CmuxDashboard, type DashboardOrphan } from "./cmux-dashboard.ts";
9
+ import { createAgentsPanel } from "./panel.ts";
9
10
  import { renderSubagentCall, renderSubagentResult } from "./render.ts";
10
11
  import type { SubagentDetails } from "./run.ts";
11
12
  import { failedToolResult, SubagentRuntime } from "./runtime.ts";
13
+ import subagentSettings from "./settings.ts";
14
+
15
+ interface SubagentSessionState {
16
+ disabled: string[];
17
+ }
18
+
19
+ const SUBAGENT_SESSION_STATE_TYPE = "tau.subagent.disabled";
12
20
 
13
21
  const params = Type.Union([
14
22
  Type.Object(
@@ -39,6 +47,7 @@ const params = Type.Union([
39
47
 
40
48
  export default function subagentExtension(pi: ExtensionAPI): void {
41
49
  const runtime = new SubagentRuntime(pi);
50
+ let sessionDisabled = new Set<string>();
42
51
  let failureNotify: ((message: string) => void) | undefined;
43
52
  const orphans: DashboardOrphan[] = [];
44
53
  const retryOrphans = async () => {
@@ -122,13 +131,74 @@ export default function subagentExtension(pi: ExtensionAPI): void {
122
131
  }
123
132
  for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
124
133
  };
134
+ const disabledAgentNames = async (ctx: ExtensionContext) => {
135
+ const configured = new Set((await loadTauExtensionSettings(ctx, subagentSettings)).disabled);
136
+ return { configured, effective: new Set([...configured, ...sessionDisabled]) };
137
+ };
125
138
  const parentVisibleAgents = async (ctx: ExtensionContext, discovery: AgentDiscovery) => {
126
- const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
139
+ const [sync, disabled] = await Promise.all([
140
+ loadTauExtensionSettings(ctx, contextSettings).then((settings) => settings.sync),
141
+ disabledAgentNames(ctx),
142
+ ]);
127
143
  const parentVisible = sync.enabled && sync.automation;
128
144
  return [...discovery.agents.values()]
129
- .filter((agent) => agent.name !== "context-sync" || parentVisible)
145
+ .filter((agent) => !disabled.effective.has(agent.name) && (agent.name !== "context-sync" || parentVisible))
130
146
  .sort((a, b) => a.name.localeCompare(b.name));
131
147
  };
148
+ const restoreSessionState = (ctx: ExtensionContext) => {
149
+ let saved: string[] | undefined;
150
+ for (const entry of ctx.sessionManager.getBranch()) {
151
+ if (entry.type !== "custom" || entry.customType !== SUBAGENT_SESSION_STATE_TYPE) continue;
152
+ const data = entry.data as { disabled?: unknown } | undefined;
153
+ if (Array.isArray(data?.disabled)) {
154
+ saved = data.disabled.filter((name): name is string => typeof name === "string" && name.length > 0);
155
+ }
156
+ }
157
+ sessionDisabled = new Set(saved ?? []);
158
+ };
159
+
160
+ pi.registerCommand("agents", {
161
+ description: "Enable/disable subagents for this session",
162
+ handler: async (_args, ctx) => {
163
+ if (ctx.mode !== "tui") {
164
+ ctx.ui.notify("/agents requires TUI mode", "error");
165
+ return;
166
+ }
167
+
168
+ const [discovery, disabled] = await Promise.all([
169
+ discoverAgents(ctx.cwd, ctx.isProjectTrusted()),
170
+ disabledAgentNames(ctx),
171
+ ]);
172
+ warn(discovery, ctx);
173
+ const agents = [...discovery.agents.values()].sort((a, b) => a.name.localeCompare(b.name));
174
+ if (agents.length === 0) {
175
+ ctx.ui.notify("No valid subagents found", "warning");
176
+ return;
177
+ }
178
+
179
+ await ctx.ui.custom((tui, theme, _keybindings, done) =>
180
+ createAgentsPanel(
181
+ tui,
182
+ theme,
183
+ agents.map((agent) => ({
184
+ id: agent.name,
185
+ disabled: disabled.effective.has(agent.name),
186
+ configured: disabled.configured.has(agent.name),
187
+ })),
188
+ (disabledNames) => {
189
+ for (const agent of agents) {
190
+ if (!disabled.configured.has(agent.name)) sessionDisabled.delete(agent.name);
191
+ }
192
+ for (const name of disabledNames) sessionDisabled.add(name);
193
+ pi.appendEntry<SubagentSessionState>(SUBAGENT_SESSION_STATE_TYPE, {
194
+ disabled: [...sessionDisabled].sort(),
195
+ });
196
+ },
197
+ () => done(undefined),
198
+ ),
199
+ );
200
+ },
201
+ });
132
202
 
133
203
  pi.on("before_agent_start", async (event, ctx) => {
134
204
  if (!pi.getActiveTools().includes("subagent")) return undefined;
@@ -147,7 +217,7 @@ Pass \`files\` when exact relevant files are already known. Tau autoreads curren
147
217
 
148
218
  Delegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory.
149
219
 
150
- Review limit: for one user task or coherent implementation batch, call the \`review\` agent at most twice. The first call is the initial review. The only permitted second call is a follow-up in the same retained thread to check fixes from that initial review. Never call \`review\` a third time for the same task. Do not call it for documentation-only work, trivial edits, small localized changes, or micro-adjustments after review; inspect and test those directly.`;
220
+ Scout only for substantial multi-hop lookup that would flood parent context. Skip few known-path reads, single declaration lookups, or small digs with most evidence already in hand; use tools directly. When uncertain, dig yourself.`;
151
221
  return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
152
222
  });
153
223
  pi.registerTool(
@@ -180,6 +250,27 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
180
250
  return failedToolResult(agent, task, "queue", parentModel, parentThinking, error, threadKey);
181
251
  }
182
252
 
253
+ if (continuing) {
254
+ const thread = runtime.listThreads(ctx.cwd).find((item) => item.id === threadKey);
255
+ if (thread) {
256
+ const disabled = await disabledAgentNames(ctx);
257
+ if (disabled.effective.has(thread.definition.name)) {
258
+ const source = disabled.configured.has(thread.definition.name)
259
+ ? "in Tau settings"
260
+ : "for this session";
261
+ return failedToolResult(
262
+ thread.definition.name,
263
+ task,
264
+ "discovery",
265
+ parentModel,
266
+ parentThinking,
267
+ `Agent ${thread.definition.name} is disabled ${source}.`,
268
+ threadKey,
269
+ );
270
+ }
271
+ }
272
+ }
273
+
183
274
  return runtime.execute({
184
275
  agent,
185
276
  task,
@@ -215,6 +306,11 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
215
306
  error: `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`,
216
307
  };
217
308
  }
309
+ const disabled = await disabledAgentNames(ctx);
310
+ if (disabled.effective.has(definition.name)) {
311
+ const source = disabled.configured.has(definition.name) ? "in Tau settings" : "for this session";
312
+ return { ok: false, phase: "discovery", error: `Agent ${definition.name} is disabled ${source}.` };
313
+ }
218
314
  if (definition.name === "context-sync") {
219
315
  const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
220
316
  if (!sync.enabled) {
@@ -257,13 +353,17 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
257
353
  const details = event.details as SubagentDetails | undefined;
258
354
  if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
259
355
  });
260
- pi.on("session_start", async () => {
356
+ pi.on("session_start", async (_event, ctx) => {
357
+ restoreSessionState(ctx);
261
358
  await runtime.reset();
262
359
  await replaceDashboard();
263
360
  rowState.clear();
264
361
  fingerprints.clear();
265
362
  failureNotify = undefined;
266
363
  });
364
+ pi.on("session_tree", (_event, ctx) => {
365
+ restoreSessionState(ctx);
366
+ });
267
367
  pi.on("session_shutdown", async () => {
268
368
  unsubscribeDashboard();
269
369
  await runtime.shutdown();
@@ -273,5 +373,6 @@ Review limit: for one user task or coherent implementation batch, call the \`rev
273
373
  rowState.clear();
274
374
  fingerprints.clear();
275
375
  failureNotify = undefined;
376
+ sessionDisabled.clear();
276
377
  });
277
378
  }
@@ -0,0 +1,124 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, getKeybindings, Key, matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
+ import {
4
+ rawHint,
5
+ SelectableList,
6
+ type SelectableListResult,
7
+ ToolPanel,
8
+ type ToolPanelConfig,
9
+ } from "@shanepadgett/tau-tui";
10
+
11
+ export interface AgentPanelItem {
12
+ id: string;
13
+ disabled: boolean;
14
+ configured: boolean;
15
+ }
16
+
17
+ export function createAgentsPanel(
18
+ tui: TUI,
19
+ theme: Theme,
20
+ initial: readonly AgentPanelItem[],
21
+ onApply: (disabled: readonly string[]) => void,
22
+ done: () => void,
23
+ ): Component {
24
+ return new AgentsPanel(tui, theme, initial, onApply, done);
25
+ }
26
+
27
+ class AgentsPanel implements Component {
28
+ private readonly tui: TUI;
29
+ private readonly onApply: (disabled: readonly string[]) => void;
30
+ private readonly done: () => void;
31
+ private readonly list: SelectableList<AgentPanelItem>;
32
+ private readonly panelConfig: ToolPanelConfig;
33
+ private readonly panel: ToolPanel;
34
+ private items: readonly AgentPanelItem[];
35
+ private closed = false;
36
+
37
+ constructor(
38
+ tui: TUI,
39
+ theme: Theme,
40
+ initial: readonly AgentPanelItem[],
41
+ onApply: (disabled: readonly string[]) => void,
42
+ done: () => void,
43
+ ) {
44
+ this.tui = tui;
45
+ this.items = initial;
46
+ this.onApply = onApply;
47
+ this.done = done;
48
+ this.list = new SelectableList(theme, {
49
+ items: initial,
50
+ emptyMessage: "No valid subagents found.",
51
+ selection: { kind: "single", primaryLabel: "apply" },
52
+ actions: [],
53
+ cancelLabel: "close",
54
+ maxVisible: 10,
55
+ renderItem: (item, state, width) => {
56
+ const label = state.active ? theme.bold(item.id) : item.id;
57
+ const status = item.configured
58
+ ? theme.fg("muted", "disabled by Tau settings")
59
+ : item.disabled
60
+ ? theme.fg("warning", "disabled")
61
+ : theme.fg("success", "enabled");
62
+ return [truncateToWidth(`${label} ${status}`, width, "…")];
63
+ },
64
+ onResult: (result) => this.handleResult(result),
65
+ });
66
+ this.panelConfig = {
67
+ title: "Subagents",
68
+ secondary: "Changes apply to this session.",
69
+ body: this.list,
70
+ footer: { kind: "hints", hints: this.hints() },
71
+ border: "box",
72
+ };
73
+ this.panel = new ToolPanel(theme, this.panelConfig);
74
+ }
75
+
76
+ render(width: number): string[] {
77
+ return this.panel.render(width);
78
+ }
79
+
80
+ invalidate(): void {
81
+ this.panel.invalidate();
82
+ }
83
+
84
+ handleInput(data: string): void {
85
+ if (matchesKey(data, Key.space) || data === " ") {
86
+ const current = this.list.getCurrentItem();
87
+ if (!current || current.configured) return;
88
+ this.items = this.items.map((item) => (item.id === current.id ? { ...item, disabled: !item.disabled } : item));
89
+ this.list.setItems(this.items, current.id);
90
+ this.syncPanel();
91
+ return;
92
+ }
93
+ if (getKeybindings().matches(data, "tui.select.confirm")) {
94
+ this.onApply(this.items.filter((item) => item.disabled && !item.configured).map((item) => item.id));
95
+ this.closed = true;
96
+ this.done();
97
+ return;
98
+ }
99
+ this.list.handleInput(data);
100
+ if (!this.closed) this.syncPanel();
101
+ }
102
+
103
+ private handleResult(result: SelectableListResult<AgentPanelItem>): void {
104
+ if (result.kind === "cancel") {
105
+ this.closed = true;
106
+ this.done();
107
+ return;
108
+ }
109
+ }
110
+
111
+ private syncPanel(): void {
112
+ this.panelConfig.footer = { kind: "hints", hints: this.hints() };
113
+ this.tui.requestRender();
114
+ }
115
+
116
+ private hints() {
117
+ const hints = this.list.getKeyHints();
118
+ const current = this.list.getCurrentItem();
119
+ if (current && !current.configured) {
120
+ hints.splice(1, 0, rawHint("Space", "toggle"));
121
+ }
122
+ return hints;
123
+ }
124
+ }
@@ -48,7 +48,8 @@ export function renderSubagentResult(
48
48
  return text;
49
49
  }
50
50
  const identity = `${details.displayName} (${details.agent})`;
51
- const header = `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", identity)} ${theme.fg("muted", `$${details.usage.cost.toFixed(4)} · ${(details.durationMs / 1000).toFixed(1)}s · ${details.toolCalls} tools`)}`;
51
+ const ctx = typeof details.contextPercent === "number" ? ` · ${details.contextPercent.toFixed(1)}% ctx` : "";
52
+ const header = `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", identity)} ${theme.fg("muted", `$${details.usage.cost.toFixed(4)} · ${(details.durationMs / 1000).toFixed(1)}s · ${details.toolCalls} tools${ctx}`)}`;
52
53
  if (!expanded) {
53
54
  text.setText(`${header} ${theme.fg("muted", details.task.replace(/\s+/g, " ").trim())}`);
54
55
  return text;
@@ -13,14 +13,10 @@ import {
13
13
  type ExtensionContext,
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import { createCompleteFileMeta } from "../../shared/full-file-knowledge.ts";
16
+ import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
16
17
  import type { AgentDefinition } from "./agents.ts";
17
18
  import { emptySubagentResumeState, type RetainedTurnOutcome, type SubagentResumeState } from "./resume.ts";
18
- import {
19
- createSubagentSessionResource,
20
- resolveSubagentSessionInputs,
21
- type SubagentSessionInputs,
22
- type SubagentSessionResource,
23
- } from "./session-resource.ts";
19
+ import { resolveSubagentSessionInputs, type SubagentSessionInputs } from "./session-resource.ts";
24
20
 
25
21
  const PREVIEW_LIMIT = 600;
26
22
  const VALUE_LIMIT = 180;
@@ -62,6 +58,7 @@ export interface SubagentDetails {
62
58
  omittedActions: number;
63
59
  omittedErrors: number;
64
60
  usage: SubagentUsage;
61
+ contextPercent?: number;
65
62
  durationMs: number;
66
63
  truncation?: {
67
64
  truncated: boolean;
@@ -85,7 +82,7 @@ export interface SubagentThread {
85
82
  displayName: string;
86
83
  definition: AgentDefinition;
87
84
  sessionInputs: SubagentSessionInputs;
88
- resource: SubagentSessionResource;
85
+ resource: IsolatedSessionResource;
89
86
  cwd: string;
90
87
  model: string;
91
88
  thinkingLevel: string;
@@ -199,7 +196,7 @@ export async function createSubagentThread(options: {
199
196
  extensionPaths: readonly string[];
200
197
  initialTask: string;
201
198
  ctx: ExtensionContext;
202
- thinkingLevel: string;
199
+ thinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
203
200
  signal: AbortSignal;
204
201
  onWarning?: (warning: string) => void;
205
202
  }): Promise<SubagentThread> {
@@ -222,7 +219,7 @@ export async function createSubagentThread(options: {
222
219
  signal,
223
220
  ...(onWarning === undefined ? {} : { onWarning }),
224
221
  });
225
- const resource = await createSubagentSessionResource(sessionInputs, signal);
222
+ const resource = await createIsolatedSessionResource(sessionInputs, signal);
226
223
  try {
227
224
  return {
228
225
  id,
@@ -307,6 +304,9 @@ export async function runSubagentTurn(options: {
307
304
  if (!force && now - lastTextUpdate < 100) return;
308
305
  lastTextUpdate = now;
309
306
  details.durationMs = now - started;
307
+ const context = session.getContextUsage();
308
+ if (typeof context?.percent === "number") details.contextPercent = context.percent;
309
+ else delete details.contextPercent;
310
310
  const snapshot = cloneSnapshot(details);
311
311
  snapshot.actions = snapshot.actions.slice(-5);
312
312
  // Detached observer chain — must not delay prompt completion.
@@ -1,5 +1,6 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
3
4
  import type { AgentDefinition } from "./agents.ts";
4
5
  import { buildColdResumePrompt, retainSubagentTurn } from "./resume.ts";
5
6
  import {
@@ -15,7 +16,6 @@ import {
15
16
  type SubagentPhase,
16
17
  type SubagentThread,
17
18
  } from "./run.ts";
18
- import { createSubagentSessionResource, type SubagentSessionResource } from "./session-resource.ts";
19
19
 
20
20
  const MAX_RETAINED_THREADS = 16;
21
21
  const GLOBAL_CONCURRENCY = 4;
@@ -216,7 +216,7 @@ export class SubagentRuntime {
216
216
  definition?: AgentDefinition;
217
217
  ctx: ExtensionContext;
218
218
  parentModel: string;
219
- parentThinking: string;
219
+ parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
220
220
  signal?: AbortSignal;
221
221
  onUpdate?: (details: SubagentDetails) => void | Promise<void>;
222
222
  resolveFreshDefinition: () => Promise<
@@ -286,7 +286,7 @@ export class SubagentRuntime {
286
286
  threadKey?: string;
287
287
  ctx: ExtensionContext;
288
288
  parentModel: string;
289
- parentThinking: string;
289
+ parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
290
290
  definition?: AgentDefinition;
291
291
  signal?: AbortSignal;
292
292
  onUpdate?: (details: SubagentDetails) => void | Promise<void>;
@@ -316,7 +316,7 @@ export class SubagentRuntime {
316
316
  let releaseGlobal: (() => void) | undefined;
317
317
  let reservedThread: TrackedThread | undefined;
318
318
  let provisionalThread: TrackedThread | undefined;
319
- let provisionalResource: SubagentSessionResource | undefined;
319
+ let provisionalResource: IsolatedSessionResource | undefined;
320
320
  let reservationToken: symbol | undefined;
321
321
  let admitAdvanced = false;
322
322
  let phase: SubagentPhase = "queue";
@@ -513,7 +513,7 @@ export class SubagentRuntime {
513
513
  phase = "startup";
514
514
  fanOut({ ...active.snapshot, status: "starting", phase: "startup", agent, threadId: thread.id }, true);
515
515
  const oldResource = thread.resource;
516
- provisionalResource = await createSubagentSessionResource(thread.sessionInputs, combined);
516
+ provisionalResource = await createIsolatedSessionResource(thread.sessionInputs, combined);
517
517
  if (!this.isLive(generation, combined) || thread.disposed || this.threads.get(thread.id) !== thread) {
518
518
  await provisionalResource.dispose();
519
519
  provisionalResource = undefined;
@@ -1,12 +1,5 @@
1
- import {
2
- createAgentSession,
3
- DefaultResourceLoader,
4
- getAgentDir,
5
- ModelRuntime,
6
- SessionManager,
7
- type AgentSession,
8
- type ExtensionContext,
9
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { resolveIsolatedSessionModel, type IsolatedSessionInputs } from "../../shared/isolated-session.ts";
10
3
  import type { AgentDefinition, ThinkingLevel } from "./agents.ts";
11
4
 
12
5
  const CHILD_UI_BLOCKED_METHODS = new Set([
@@ -18,25 +11,10 @@ const CHILD_UI_BLOCKED_METHODS = new Set([
18
11
  "setWorkingIndicator",
19
12
  ]);
20
13
 
21
- type SelectedModel = NonNullable<ExtensionContext["model"]>;
22
- type SelectedProvider = NonNullable<ReturnType<ExtensionContext["modelRegistry"]["getProvider"]>>;
23
-
24
- export interface SubagentSessionInputs {
14
+ export interface SubagentSessionInputs extends IsolatedSessionInputs {
25
15
  definition: AgentDefinition;
26
- extensionPaths: readonly string[];
27
- cwd: string;
28
- model: SelectedModel;
29
16
  modelName: string;
30
- provider: SelectedProvider;
31
- runtimeApiKey: string | undefined;
32
17
  thinkingLevel: ThinkingLevel;
33
- bindTarget: { mode: "print" } | { mode: "tui"; uiContext: ExtensionContext["ui"] };
34
- }
35
-
36
- export interface SubagentSessionResource {
37
- readonly inputs: SubagentSessionInputs;
38
- readonly session: AgentSession;
39
- dispose(): Promise<void>;
40
18
  }
41
19
 
42
20
  function childUiContext(ui: ExtensionContext["ui"]): ExtensionContext["ui"] {
@@ -53,117 +31,31 @@ export async function resolveSubagentSessionInputs(options: {
53
31
  definition: AgentDefinition;
54
32
  extensionPaths: readonly string[];
55
33
  ctx: ExtensionContext;
56
- parentThinkingLevel: string;
34
+ parentThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
57
35
  signal: AbortSignal;
58
36
  onWarning?: (warning: string) => void;
59
37
  }): Promise<SubagentSessionInputs> {
60
38
  const { definition, extensionPaths, ctx, signal, onWarning } = options;
61
- let model = ctx.model;
62
- let thinkingLevel = options.parentThinkingLevel as ThinkingLevel;
63
- let selectedAuth: Awaited<ReturnType<ExtensionContext["modelRegistry"]["getApiKeyAndHeaders"]>> | undefined;
64
- if (definition.model) {
65
- const separator = definition.model.indexOf("/");
66
- const configured = ctx.modelRegistry.find(
67
- definition.model.slice(0, separator),
68
- definition.model.slice(separator + 1),
69
- );
70
- if (!configured) onWarning?.(`model ${definition.model} is unavailable; using parent model`);
71
- else {
72
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
73
- if (!auth.ok) onWarning?.(`model ${definition.model} is unavailable: ${auth.error}; using parent model`);
74
- else {
75
- model = configured;
76
- selectedAuth = auth;
77
- }
78
- }
79
- }
80
- if (definition.thinking) {
81
- const mapped = model?.thinkingLevelMap?.[definition.thinking];
82
- const unsupported =
83
- !model?.reasoning ||
84
- mapped === null ||
85
- ((definition.thinking === "xhigh" || definition.thinking === "max") && mapped === undefined);
86
- if (unsupported)
87
- onWarning?.(`thinking ${definition.thinking} is unavailable for the selected model; using parent thinking`);
88
- else thinkingLevel = definition.thinking;
89
- }
90
- if (!model) throw new Error(`Agent ${definition.name} startup failed: parent has no model`);
91
- const auth = selectedAuth ?? (await ctx.modelRegistry.getApiKeyAndHeaders(model));
92
- if (!auth.ok) throw new Error(`Agent ${definition.name} startup failed: ${auth.error}`);
93
- const provider = ctx.modelRegistry.getProvider(model.provider);
94
- if (!provider) throw new Error(`Agent ${definition.name} startup failed: provider ${model.provider} is unavailable`);
95
- if (signal.aborted) throw new Error(`Agent ${definition.name} startup aborted`);
39
+ const selected = await resolveIsolatedSessionModel({
40
+ label: `Agent ${definition.name}`,
41
+ preferredModel: definition.model,
42
+ preferredThinkingLevel: definition.thinking,
43
+ usePreferredThinkingAfterModelFallback: true,
44
+ ctx,
45
+ parentThinkingLevel: options.parentThinkingLevel,
46
+ signal,
47
+ ...(onWarning === undefined ? {} : { onWarning }),
48
+ });
96
49
  return {
50
+ ...selected,
51
+ label: `Agent ${definition.name}`,
97
52
  definition,
98
53
  extensionPaths: [...extensionPaths],
99
54
  cwd: ctx.cwd,
100
- model,
101
- modelName: `${model.provider}/${model.id}`,
102
- provider,
103
- runtimeApiKey:
104
- auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model) ? auth.apiKey : undefined,
105
- thinkingLevel,
55
+ modelName: `${selected.model.provider}/${selected.model.id}`,
56
+ tools: definition.tools,
57
+ customTools: [],
106
58
  bindTarget:
107
59
  ctx.mode === "tui" && ctx.hasUI ? { mode: "tui", uiContext: childUiContext(ctx.ui) } : { mode: "print" },
108
60
  };
109
61
  }
110
-
111
- export async function createSubagentSessionResource(
112
- inputs: SubagentSessionInputs,
113
- signal: AbortSignal,
114
- ): Promise<SubagentSessionResource> {
115
- let session: AgentSession | undefined;
116
- try {
117
- if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
118
- const modelRuntime = await ModelRuntime.create();
119
- modelRuntime.registerNativeProvider(inputs.provider);
120
- if (inputs.runtimeApiKey !== undefined)
121
- await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
122
- if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
123
- const resourceLoader = new DefaultResourceLoader({
124
- cwd: inputs.cwd,
125
- agentDir: getAgentDir(),
126
- noExtensions: true,
127
- additionalExtensionPaths: [...inputs.extensionPaths],
128
- });
129
- await resourceLoader.reload();
130
- if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
131
- const created = await createAgentSession({
132
- cwd: inputs.cwd,
133
- model: inputs.model,
134
- modelRuntime,
135
- thinkingLevel: inputs.thinkingLevel,
136
- tools: inputs.definition.tools,
137
- excludeTools: ["subagent"],
138
- resourceLoader,
139
- sessionManager: SessionManager.inMemory(inputs.cwd),
140
- });
141
- session = created.session;
142
- if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
143
- await session.bindExtensions(inputs.bindTarget);
144
- if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
145
- const active = session.getActiveToolNames().sort();
146
- const expected = [...inputs.definition.tools].sort();
147
- if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
148
- const missing = expected.filter((tool) => !active.includes(tool));
149
- throw new Error(
150
- `Agent ${inputs.definition.name} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
151
- );
152
- }
153
- let disposed = false;
154
- return {
155
- inputs,
156
- session,
157
- async dispose() {
158
- if (disposed) return;
159
- disposed = true;
160
- if (session?.isStreaming) await session.abort().catch(() => undefined);
161
- session?.dispose();
162
- },
163
- };
164
- } catch (error) {
165
- if (session?.isStreaming) await session.abort().catch(() => undefined);
166
- session?.dispose();
167
- throw error;
168
- }
169
- }
@@ -0,0 +1,18 @@
1
+ import { Type } from "typebox";
2
+ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
3
+
4
+ export default defineTauExtensionSettings({
5
+ key: "subagent",
6
+ defaults: { disabled: [] as string[] },
7
+ schema: Type.Object(
8
+ {
9
+ disabled: Type.Optional(
10
+ Type.Array(Type.String({ minLength: 1 }), {
11
+ default: [],
12
+ description: "Agent names unavailable for delegation.",
13
+ }),
14
+ ),
15
+ },
16
+ { additionalProperties: false },
17
+ ),
18
+ });
@@ -32,7 +32,7 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
32
32
 
33
33
  ## context
34
34
 
35
- Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync (optional nudge). Escape cancels a running manual sync. When `sync.automation` is on, the coding agent can also run the `context-sync` subagent after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is the master switch for command, automation, and validation auto-run. Entry `files` are autoread; entry `anchors` are unloaded navigation paths. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
35
+ Adds `/context` to set branch-local reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync. Active entries produce one ephemeral per-call projection instead of transcript messages. Entry `read` paths supply exact contents, `outline` paths use Explore, and `references` stay unloaded. Clear all selections and confirm to remove active context. Escape cancels a running manual sync. When `sync.automation` is on, coding agent can also run `context-sync` after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is master switch for command, automation, and validation auto-run. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
36
36
 
37
37
  ## working-memory
38
38
 
@@ -74,6 +74,10 @@ Adds `/publish` to create a tagged release, trigger trusted npm publishing in Gi
74
74
 
75
75
  Adds `/qna` for when the agent has asked you several questions in chat and you want a friendly UI for answering them on your own terms. It is only active when you manually run the command.
76
76
 
77
+ ## review
78
+
79
+ Adds `/review` for explicit isolated review of current Git changes. Choose `simplify`, `architecture`, or `correctness`, or run a mode directly. Results stay outside agent context until you send them from result view, and can be exported under `.pi/tau/reviews/`. `/review show` reopens latest result on current session branch.
80
+
77
81
  ## reference
78
82
 
79
83
  Adds `/reference` to manage separate repositories kept outside the current project for inspiration or comparison. Add one with `/reference new <git-url>`, update it, switch its referenced branch, or open it in an editor. Select references and explain why they matter; Tau then puts their paths and that reason into the editor for the agent. References stay outside the project so the agent does not wander into unrelated code unless you explicitly point it there.
@@ -86,6 +90,10 @@ Shows a compact display-only marker after each run with wall time and model cost
86
90
 
87
91
  Supplies the agent with the current local date and an initial root directory snapshot as hidden session context.
88
92
 
93
+ ## script-runner
94
+
95
+ Gives the agent a first-class `script_runner` tool to execute Python and TypeScript instead of bash. On failure it returns a `scriptId`; the agent retries with targeted `{oldText,newText}` edits against the script it already wrote rather than resending the whole script. Languages are detected from the environment (Python via `python3`/`python`; TypeScript via Node `--experimental-strip-types`, Node 22.6+). The tool registers only available languages and is hidden from the prompt if neither is present.
96
+
89
97
  ## silent-command-runner
90
98
 
91
99
  Runs configured commands while keeping their output out of agent context when that is useful.
@@ -100,7 +108,7 @@ Adds `Alt+S` to stash the current prompt draft and `/pop` to browse stashed draf
100
108
 
101
109
  ## subagent
102
110
 
103
- Gives Tau a subagent delegation tool for isolated, focused work. The built-in `review` agent performs adversarial, read-only code reviews; `scout` finds local files, symbols, data flow, constraints, and unknowns without changing anything; `web-research` handles external research. Known files can be autoread as line-numbered snapshots into a fresh or retained child turn. Tau can continue a retained child thread when follow-up work depends on its prior reads and reasoning. You can also create your own subagents in the supported subagent directories. Ask Tau how to do it and have it consult the extension’s own documentation; the built-in agents show the pattern. Each subagent can register its own model, tools, and pool of display names. Reused pool names get numeric suffixes. In interactive cmux sessions, Tau opens one temporary Markdown dashboard for live subagent progress; it does not change how children run and closes shortly after the active cohort finishes.
111
+ Gives Tau a subagent delegation tool for isolated, focused work. Run `/agents` to enable or disable individual agents for the current session, or set `extensions.subagent.disabled` in Tau settings for a persistent choice. `scout` is substantial multi-hop local code lookup that would chew parent context; facts only, not small digs; `web-research` handles external research. Known files can be autoread as line-numbered snapshots into a fresh or retained child turn. Tau can continue a retained child thread when follow-up work depends on its prior reads and reasoning. You can also create your own subagents in supported subagent directories. Ask Tau how to do it and have it consult extension documentation; built-in agents show pattern. Each subagent can register its own model, tools, and pool of display names. Reused pool names get numeric suffixes. In interactive cmux sessions, Tau opens one temporary Markdown dashboard for live subagent progress; it does not change how children run and closes shortly after active cohort finishes.
104
112
 
105
113
  ## tau-help
106
114
 
@@ -114,10 +122,6 @@ Adds `/tau`, `/tau init [--global|--project]`, and `/tau doctor` for Tau setup a
114
122
 
115
123
  Progressively exposes specialist tools through `load_tools`. Tau normally loads the fixed `web`, `image`, and `appshot` groups itself when needed; supported providers can preserve more prompt-cache reuse.
116
124
 
117
- ## turn-budget
118
-
119
- Tracks and limits agent turns to keep work bounded.
120
-
121
125
  ## web
122
126
 
123
127
  Gives the agent compact `websearch`, `webfetch`, and `codesearch` tools for web and implementation research.