@shanepadgett/tau-agent 0.28.1 → 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 (66) hide show
  1. package/docs/context.md +14 -9
  2. package/docs/subagents.md +7 -5
  3. package/extensions/cache-diagnostics/index.ts +4 -5
  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/README.md +1 -1
  12. package/extensions/explore/ast/read/hook.ts +5 -35
  13. package/extensions/explore/ast/read/policy.ts +0 -32
  14. package/extensions/explore/index.ts +2 -0
  15. package/extensions/explore/outline-injection.ts +151 -0
  16. package/extensions/explore/settings.ts +0 -8
  17. package/extensions/ideas/browser.ts +27 -16
  18. package/extensions/review/README.md +11 -0
  19. package/extensions/review/index.ts +135 -0
  20. package/extensions/review/model.ts +144 -0
  21. package/extensions/review/panel.ts +128 -0
  22. package/extensions/review/session.ts +106 -0
  23. package/extensions/runtime-context/README.md +1 -1
  24. package/extensions/runtime-context/index.ts +3 -66
  25. package/extensions/script-runner/README.md +7 -0
  26. package/extensions/script-runner/index.ts +275 -0
  27. package/extensions/stash/browser.ts +30 -18
  28. package/extensions/subagent/README.md +17 -4
  29. package/extensions/subagent/agents/context-sync.md +2 -2
  30. package/extensions/subagent/agents/scout.md +49 -52
  31. package/extensions/subagent/agents.ts +0 -1
  32. package/extensions/subagent/cmux-dashboard.ts +7 -3
  33. package/extensions/subagent/index.ts +105 -4
  34. package/extensions/subagent/panel.ts +124 -0
  35. package/extensions/subagent/render.ts +2 -1
  36. package/extensions/subagent/run.ts +9 -9
  37. package/extensions/subagent/runtime.ts +5 -5
  38. package/extensions/subagent/session-resource.ts +19 -127
  39. package/extensions/subagent/settings.ts +18 -0
  40. package/extensions/tau-help/help.md +12 -8
  41. package/extensions/working-memory/README.md +9 -0
  42. package/extensions/working-memory/checkpoint.ts +175 -0
  43. package/extensions/working-memory/index.ts +338 -0
  44. package/extensions/working-memory/memory.ts +266 -0
  45. package/extensions/working-memory/render.ts +178 -0
  46. package/extensions/working-memory/settings.ts +38 -0
  47. package/extensions/working-memory/state.ts +152 -0
  48. package/package.json +2 -2
  49. package/schemas/tau.schema.json +35 -55
  50. package/shared/context-messages.ts +19 -0
  51. package/shared/events.ts +5 -0
  52. package/shared/full-file-knowledge.ts +0 -1
  53. package/shared/injected-context.ts +2 -2
  54. package/shared/isolated-session.ts +151 -0
  55. package/shared/outline-injection.ts +56 -0
  56. package/extensions/context-pruning/README.md +0 -39
  57. package/extensions/context-pruning/index.ts +0 -382
  58. package/extensions/context-pruning/projection.ts +0 -60
  59. package/extensions/context-pruning/prune.ts +0 -199
  60. package/extensions/context-pruning/render.ts +0 -251
  61. package/extensions/context-pruning/settings.ts +0 -39
  62. package/extensions/subagent/agents/review.md +0 -68
  63. package/extensions/turn-budget/README.md +0 -14
  64. package/extensions/turn-budget/index.ts +0 -116
  65. package/extensions/turn-budget/settings.ts +0 -35
  66. package/shared/context-pruning-state.ts +0 -152
@@ -8,17 +8,30 @@ Each fresh child also gets a display name from its agent definition. The name st
8
8
 
9
9
  Tau includes these built-in agents:
10
10
 
11
- - `review` performs adversarial, read-only code review for correctness, runtime risks, duplication, and over- or under-engineering.
12
- - `scout` finds local files, symbols, data flow, constraints, and unknowns without changing anything.
11
+ - `scout` does substantial multi-hop local code lookup that would chew parent context; paths, declarations, imports, references, call edges; facts only. Skip small digs.
13
12
  - `web-research` researches web and code sources with `websearch`, `codesearch`, and `webfetch`.
14
13
  - `context-sync` maps meaningful uncommitted work into `.pi/contexts`. Agent-driven use is `extensions.context.sync.automation` (requires `sync.enabled`). `/context-sync` is the manual/nudge path when sync is enabled. Validation can auto-run it when validation and sync are enabled.
15
14
 
16
15
  Ask Tau to delegate a task, or let it call `subagent` with an agent name and task. Children use the parent's current working directory and inherit its model and thinking level unless their definition overrides either value. They do not receive the parent conversation. Tau loads only the extensions that own a child's declared tools, so unrelated extension hooks do not run in child sessions. When a child must inspect another repository, put its exact absolute path in the delegated task.
17
16
 
18
- When the relevant files are already known, pass them with the call so Tau can autoread them into that child turn:
17
+ Run `/agents` to enable or disable agents for the current session. Press Space to stage each toggle, then Enter to apply the changes. Session choices follow the current session branch and do not change Tau settings. Agents disabled in Tau settings appear as `disabled by Tau settings` and cannot be enabled from this command. Disable agents persistently with `extensions.subagent.disabled`:
18
+
19
+ ```json
20
+ {
21
+ "extensions": {
22
+ "subagent": {
23
+ "disabled": ["web-research"]
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ Disabled agents are hidden from the parent prompt and cannot start or continue a child thread.
30
+
31
+ When relevant files are already known, pass them with the call so Tau can autoread them into that child turn:
19
32
 
20
33
  ```text
21
- subagent({ agent: "review", task: "Review the runtime change", files: ["src/runtime.ts", "test/runtime.test.ts"] })
34
+ subagent({ agent: "scout", task: "Trace the runtime change", files: ["src/runtime.ts", "test/runtime.test.ts"] })
22
35
  ```
23
36
 
24
37
  Paths may be relative to the parent's current working directory or absolute. Tau reads current snapshots when the turn starts and includes line numbers so the child can cite them without another read. Missing files appear as failed autoread context; they do not stop the child. Keep the list focused because the complete snapshots use the child's context window. Files can also be supplied on a retained-thread follow-up.
@@ -23,7 +23,7 @@ thinking: high
23
23
 
24
24
  You maintain the living repository context map under `.pi/contexts`.
25
25
 
26
- Tabs/folders are domains. TOML files are concepts. TOML sections are selectable work-scope entries. Entry `files` are eager autoread paths. Entry `anchors` are lazy navigation paths. Preserve an existing path's loading class when it already appears anywhere in the catalog. New paths default to eager `files`.
26
+ Tabs/folders are domains. TOML files are concepts. TOML sections are selectable work-scope entries. Every entry has three explicit loading modes: `read` for exact complete contents, `outline` for structural Explore outlines, and `references` for unloaded navigation paths. Preserve an existing path's loading mode when it already appears anywhere in the catalog. New paths default to `references`. Promote recurring source entry points to `outline`. Use `read` only when exact wording is routinely required, such as repository instructions or a small authoritative specification.
27
27
 
28
28
  ## Tools
29
29
 
@@ -65,7 +65,7 @@ Before placing any path, answer out loud in order:
65
65
  2. **Concept** — Inside that domain, which subsystem TOML? Reuse, new, split, or merge?
66
66
  3. **Entry** — Which work scope? Update, new, split, delete, or move between concepts/domains?
67
67
  4. **Bloat** — Did this touch make an entry/concept a junk drawer? Split now if yes.
68
- 5. **Membership** — Assign files/anchors only under the winners. Every eligible changed non-deleted file must belong somewhere. Remove every stale catalog path.
68
+ 5. **Membership** — Assign read/outline/references only under the winners. Every entry must contain all three arrays, even when an array is empty. Every eligible changed non-deleted file must belong somewhere. Remove every stale catalog path.
69
69
 
70
70
  Path stuffing into the nearest feature bucket without climbing the ladder is failure.
71
71
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scout
3
- description: Tiered, AST-first local discovery of files, declarations, data flow, constraints, and unknowns without changes
3
+ description: "Substantial multi-hop local code lookup that would chew parent context; paths, declarations, imports, references, call edges; facts only. Skip small digs"
4
4
  tools:
5
5
  - read
6
6
  - bash
@@ -14,8 +14,7 @@ tools:
14
14
  - callees
15
15
  - references
16
16
  - implementations
17
- - impact
18
- - context
17
+ - working_memory
19
18
  names:
20
19
  - Pathfinder
21
20
  - Trailblazer
@@ -26,33 +25,43 @@ model: openai-codex/gpt-5.6-luna
26
25
  thinking: high
27
26
  ---
28
27
 
29
- Stay inside task. Answer only what was asked. No mutations, side quests, background sweeps, or unasked advice.
28
+ You are a read-only repository retrieval worker. Locate requested source evidence and return exact cited facts.
30
29
 
31
- Delegating prompt controls output. Otherwise use smallest matching shape below.
30
+ Do not diagnose bugs, explain causes, infer runtime behavior, evaluate correctness, assess consequences, recommend changes, choose between alternatives, or make design decisions. The parent agent owns all interpretation and judgment.
31
+
32
+ If a task mixes lookup with judgment, perform only its concrete lookup portion and list the unanswered judgment under `Parent question`. If no concrete lookup exists, return `Parent question:` followed by the request. Do not attempt to answer it.
33
+
34
+ Stay inside task. No mutations, side quests, background sweeps, or unasked advice.
35
+
36
+ ## Allowed work
37
+
38
+ - Find files, declarations, literals, configuration values, registrations, and tests.
39
+ - List imports, references, callers, callees, implementations, and other direct syntactic relationships.
40
+ - Retrieve exact signatures or declaration bodies requested by parent.
41
+ - Confirm whether an exact source pattern exists within a stated scope.
42
+ - Report ambiguity or missing evidence without resolving it through inference.
32
43
 
33
44
  ## Evidence ladder
34
45
 
35
- Use cheapest source that proves each claim. Skip steps when task supplies exact path or declaration. Escalate only when current evidence cannot answer.
46
+ Use cheapest source that proves each returned fact. Skip steps when task supplies exact path or declaration. Escalate only when current evidence cannot complete requested lookup.
36
47
 
37
48
  1. **Supplied context** — Treat current line-numbered task files as authoritative this turn.
38
49
  2. **Paths and literals** — Use read-only `bash` (`ls`, `find`, `rg`/`grep`) for narrow path discovery, exact text, registrations, and unsupported formats. Use ranged `read` for formatting or source without structural support.
39
- 3. **Structure** — Default to `outline` for known files/packages and unfamiliar supported subtrees. Use `discover` when reuse intent is known but path or exact name is not. Use `ast_search` for source shapes.
40
- 4. **Exact declarations** — Use `show` with path + name (+ line when needed). Prefer `signature`; add docs, body, imports, or context lines only when question requires them.
41
- 5. **Focused relationships** — After resolving a declaration, use `callers`, `callees`, `references`, or `implementations` for one direct relationship question. Use `deps` and `reverse_deps` for file imports, not declaration calls.
42
- 6. **Composition** — Use `impact` for full one-hop declaration plus transitive file blast radius. Use `context` for one budgeted declaration pack when nearby bodies and relationships answer faster than separate calls.
50
+ 3. **Structure** — Default to `outline` for known files/packages and unfamiliar supported subtrees. Use `discover` when requested declaration path or exact name is unknown. Use `ast_search` for source shapes.
51
+ 4. **Exact declarations** — Use `show` with path + name (+ line when needed). Prefer `signature`; add docs, body, imports, or context lines only when explicitly required.
52
+ 5. **Direct relationships** — After resolving a declaration, use `callers`, `callees`, `references`, or `implementations` for one direct relationship lookup. Use `deps` and `reverse_deps` for file imports, not declaration calls.
43
53
 
44
- Structural results prove bounded syntax, not runtime dispatch. Preserve exact, inferred, and ambiguous labels. Do not turn ambiguous sites into claimed impact.
54
+ Structural results prove bounded syntax, not runtime dispatch. Preserve exact, inferred, and ambiguous labels emitted by tools. Never convert an ambiguous result into a fact.
45
55
 
46
- ## Search procedure
56
+ ## Search discipline
47
57
 
48
- 1. Extract target, question, scope, and requested output shape.
49
- 2. List required claims and select cheapest evidence for each.
50
- 3. Start from supplied paths and names. Search outward only for required relationships.
51
- 4. For reuse, run `discover`, then inspect selected candidates with `show`.
52
- 5. For unknown source shape, run `ast_search`, then inspect only selected enclosing declarations.
53
- 6. For behavior or data flow, orient target, follow focused relationships, then retrieve only declarations needed to explain flow.
54
- 7. For impact, use `impact`; use focused relationship tools only when one section needs closer evidence.
55
- 8. Stop when requested claims are supported. Put material gaps under `Unknowns`.
58
+ - Extract concrete target, lookup type, scope, and requested output shape.
59
+ - Narrow each call around one missing fact. Prefer structural summaries and signatures over full source.
60
+ - Start from supplied paths and names. Search outward only as needed to locate requested evidence.
61
+ - Batch only independent lookups whose results will stay small.
62
+ - Do not fan out across plausible explanations or collect evidence for a theory.
63
+ - Stop when requested evidence has been found or bounded search cannot find it.
64
+ - During a long inventory, use `working_memory` only when stale evidence would burden the remaining lookup. Do not checkpoint a small search.
56
65
 
57
66
  Absolute paths may point to read-only reference repositories outside cwd.
58
67
 
@@ -62,49 +71,37 @@ Use relevant sections only. Omit empty sections.
62
71
 
63
72
  ### Locate
64
73
 
65
- `path:start-end` — declaration — match reason
66
-
67
- ### Explain behavior
74
+ `path:start-end` — declaration or match — exact reason it matches
68
75
 
69
- - `Entry:` `path:start-end` — declaration
70
- - `Flow:` ordered steps; one cited fact each
71
- - `Result:` observed outcome
72
-
73
- ### Trace data
74
-
75
- - `Source:` cited origin
76
- - `Transforms:` ordered, cited transformations
77
- - `Consumers:` cited uses
78
-
79
- ### Find references or impact
76
+ ### Inventory
80
77
 
81
- - `Direct references:` cited relationships with certainty
82
- - `Editable scopes:` declarations requiring inspection or change
83
- - `Behavior affected:` evidence-backed consequences
84
- - `Unknowns:` remaining uncertainty
78
+ `path:start-end` — declaration or match — source-defined role
85
79
 
86
- ### Verify a claim
80
+ State searched scope when completeness matters.
87
81
 
88
- - `Verdict:` `yes`, `no`, `partially`, or `unknown`
89
- - `Evidence:` cited facts
90
- - `Qualification:` only when needed
82
+ ### Direct relationships
91
83
 
92
- ### Compare
84
+ - `Relationship:` caller, callee, import, reference, or implementation
85
+ - `Source:` cited declaration
86
+ - `Target:` cited declaration
87
+ - `Certainty:` exact or ambiguous
93
88
 
94
- - `Shared:` cited similarities
95
- - `Differences:` cited by aspect
96
- - `Relevant consequence:` requested consequences only
89
+ ### Exact pattern check
97
90
 
98
- ### Inventory
91
+ - `Found:` yes or no within searched scope
92
+ - `Scope:` paths or subtree searched
93
+ - `Matches:` exact citations when found
99
94
 
100
- `path:start-end` — declaration — role
95
+ ### Unresolved
101
96
 
102
- When completeness matters, state searched scope. If uncertain, say why.
97
+ - `Missing evidence:` requested lookup that could not be found
98
+ - `Ambiguity:` competing exact matches the tools could not disambiguate
99
+ - `Parent question:` diagnosis, explanation, evaluation, consequence, recommendation, or decision left to parent
103
100
 
104
101
  ## Reporting rules
105
102
 
106
- - Every material code claim needs exact path, line range, and declaration when one exists.
103
+ - Every returned code fact needs exact path and line range. Include declaration name when one exists.
107
104
  - Cite ranges returned by tools. Never estimate line numbers.
108
- - Separate fact from inference. Label inference.
109
105
  - Quote smallest useful fragment.
110
- - No preamble, search log, generic repository summary, repeated evidence, or unasked next steps.
106
+ - Describe only what source directly contains or what a structural tool directly reports.
107
+ - No preamble, search log, repository summary, causal explanation, conclusions, or next-step advice.
@@ -138,7 +138,6 @@ async function loadScope(
138
138
  if (!required) return new Map();
139
139
  const reason = error instanceof Error ? error.message : "directory unavailable";
140
140
  return new Map([
141
- ["review", [{ path: directory, name: "review", reason: `packaged agents unavailable: ${reason}` }]],
142
141
  [
143
142
  "web-research",
144
143
  [{ path: directory, name: "web-research", reason: `packaged agents unavailable: ${reason}` }],
@@ -124,13 +124,17 @@ export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSn
124
124
  lines.push("_No subagents._", "");
125
125
  return lines.join("\n");
126
126
  }
127
- lines.push("| Agent | State | Last tool | Calls | Time |", "| --- | --- | --- | ---: | ---: |");
127
+ lines.push(
128
+ "| Agent | State | Last tool | Calls | Cost | Ctx | Time |",
129
+ "| --- | --- | --- | ---: | ---: | ---: | ---: |",
130
+ );
128
131
  for (const details of ordered) {
129
132
  const latest = details.actions.at(-1);
130
133
  const currentTool = details.currentActivity?.match(/^\S+/)?.[0];
131
134
  const lastTool = currentTool ?? latest?.tool ?? "";
135
+ const ctx = typeof details.contextPercent === "number" ? `${details.contextPercent.toFixed(1)}%` : "—";
132
136
  lines.push(
133
- `| ${tableCell(`${details.displayName} (${details.agent})`, 48)} | ${dashboardState(details.status)} | ${tableCell(lastTool, 32) || "—"} | ${details.toolCalls} | ${elapsed(details.durationMs)} |`,
137
+ `| ${tableCell(`${details.displayName} (${details.agent})`, 48)} | ${dashboardState(details.status)} | ${tableCell(lastTool, 32) || "—"} | ${details.toolCalls} | $${details.usage.cost.toFixed(4)} | ${ctx} | ${elapsed(details.durationMs)} |`,
134
138
  );
135
139
  }
136
140
  lines.push("", "## Inputs", "");
@@ -138,7 +142,7 @@ export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSn
138
142
  lines.push(
139
143
  `### ${tableCell(details.displayName, 80)}`,
140
144
  "",
141
- `${tableCell(details.agent, 80)} · ${tableCell(details.invocationId, 80)}`,
145
+ tableCell(details.agent, 80),
142
146
  "",
143
147
  quote(details.task) || "> _(empty)_",
144
148
  );
@@ -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.