@shanepadgett/tau-agent 0.29.0 → 0.31.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 (46) 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 +11 -7
  34. package/extensions/working-memory/README.md +1 -1
  35. package/extensions/working-memory/checkpoint.ts +3 -4
  36. package/extensions/working-memory/index.ts +25 -7
  37. package/extensions/working-memory/memory.ts +53 -82
  38. package/package.json +2 -2
  39. package/schemas/tau.schema.json +9 -23
  40. package/shared/context-messages.ts +19 -0
  41. package/shared/injected-context.ts +2 -2
  42. package/shared/isolated-session.ts +151 -0
  43. package/extensions/subagent/agents/review.md +0 -75
  44. package/extensions/turn-budget/README.md +0 -14
  45. package/extensions/turn-budget/index.ts +0 -116
  46. package/extensions/turn-budget/settings.ts +0 -35
@@ -32,11 +32,11 @@ 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
 
39
- Gives agent `working_memory` for selective hard checkpoints. Model-only references identify useful conversation evidence and complete tool exchanges. Requested source files return as structural outlines, while deferred files remain cheap conditional reminders. Everything else before checkpoint leaves future model input without changing saved session. Advisory reminders begin at 40k active-context tokens. Run `/prune` to request reassessment manually.
39
+ Gives agent `working_memory` for selective hard checkpoints. Model-only references identify useful user messages and visible assistant text. Requested source files return as structural outlines, deferred files remain cheap conditional reminders, and a continuation note carries conclusions extracted from exploration. Tool history and full file reads leave future model input without changing saved session. Advisory reminders begin at 40k active-context tokens. Run `/prune` to request reassessment manually.
40
40
 
41
41
  ## explore
42
42
 
@@ -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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Working Memory gives agent selective checkpoints without changing saved conversation.
4
4
 
5
- `working_memory` keeps referenced conversation evidence and complete tool exchanges, carries requested source files as structural outlines, and records deferred files as conditional reminders. Everything else before checkpoint leaves future model context.
5
+ `working_memory` keeps referenced user messages and visible assistant text, carries requested source files as structural outlines, and records deferred files as conditional reminders. Tool history and full file reads leave model context; useful findings from them are distilled into a compact continuation note.
6
6
 
7
7
  Automatic reminders begin at 40,000 active-context tokens and remain advisory. Run `/prune` to request reassessment manually.
8
8
 
@@ -3,7 +3,7 @@ import { type ExtensionAPI, type ExtensionContext, type SessionEntry } from "@ea
3
3
  import { type Static, Type } from "typebox";
4
4
  import { truncateBoundedHead } from "../../shared/bounded-text-result.ts";
5
5
  import { requestOutlineInjections, type PreparedOutlineInjection } from "../../shared/outline-injection.ts";
6
- import { buildMemoryCatalog } from "./memory.ts";
6
+ import { buildMemoryCatalog, collectPrunedRowIds } from "./memory.ts";
7
7
  import { WORKING_MEMORY_TOOL, type DeferredFile, type WorkingMemoryCheckpointDetailsV1 } from "./state.ts";
8
8
 
9
9
  const PATH = Type.String({ minLength: 1, maxLength: 500, pattern: "\\S" });
@@ -58,7 +58,7 @@ export async function executeWorkingMemory(options: ExecuteWorkingMemoryOptions)
58
58
  const unit = catalog.get(ref);
59
59
  return unit ? [unit] : [];
60
60
  })
61
- .sort((left, right) => left.order - right.order || left.suborder - right.suborder);
61
+ .sort((left, right) => left.order - right.order);
62
62
  const retainedRefs = retained.map((unit) => unit.ref);
63
63
  const warnings = requestedRefs
64
64
  .filter((ref) => !catalog.has(ref))
@@ -89,9 +89,8 @@ export async function executeWorkingMemory(options: ExecuteWorkingMemoryOptions)
89
89
  }
90
90
 
91
91
  const anchorIndex = findAnchorEntry(branch, options.toolCallId);
92
- const retainedSet = new Set(retainedRefs);
93
92
  const preAnchorUnits = [...catalog.values()].filter((unit) => unit.order < anchorIndex);
94
- const prunedRowIds = [...new Set(preAnchorUnits.flatMap((unit) => (retainedSet.has(unit.ref) ? [] : unit.rowIds)))];
93
+ const prunedRowIds = collectPrunedRowIds(branch, anchorIndex);
95
94
  const details: WorkingMemoryCheckpointDetailsV1 = {
96
95
  v: 1,
97
96
  anchorToolCallId: options.toolCallId,
@@ -22,7 +22,7 @@ import { replayWorkingMemoryState, WORKING_MEMORY_TOOL, type WorkingMemoryCheckp
22
22
  const NUDGE_TYPE = "tau.working-memory.nudge";
23
23
  const BASELINE_TYPE = "tau.working-memory.nudge-baseline";
24
24
  const TOOL_DESCRIPTION =
25
- "Create a selective hard checkpoint for future model context. Keep valuable referenced evidence and complete tool exchanges, carry file structure as outlines, defer conditionally relevant files without reading them, and provide one compact continuation note. Never checkpoint useful evidence merely to reread it.";
25
+ "Create a selective hard checkpoint for future model context. Keep valuable user and visible assistant messages, carry file structure as outlines, defer conditionally relevant files without reading them, and distill exploration findings into one compact continuation note.";
26
26
 
27
27
  interface NudgeState {
28
28
  anchorToolCallId: string | undefined;
@@ -98,9 +98,9 @@ export default function workingMemoryExtension(pi: ExtensionAPI): void {
98
98
  promptSnippet: "Reassess and selectively checkpoint active working memory",
99
99
  promptGuidelines: [
100
100
  "Use working_memory when stale evidence has accumulated or a memory reminder asks for reassessment; continue coherent exploration when current evidence remains useful.",
101
- "A hidden working-memory reference catalog provides keep refs. Message refs preserve conversational evidence; tool refs preserve one complete call/result exchange.",
102
- "Keep active or expensive evidence, outline files when structure suffices, defer paths whose relevance is conditional, and discard evidence with no expected value.",
103
- "Everything before working_memory leaves future model context unless selected in keep. Put durable decisions, constraints, unresolved matters, and next action in continuation without duplicating retained evidence.",
101
+ "A hidden working-memory reference catalog provides keep refs only for user messages and visible assistant text. Tool calls, tool results, hidden reasoning, and framework messages cannot be retained.",
102
+ "Outline files that remain in the repository working set, defer paths whose relevance is conditional, and discard exploration history after extracting its useful conclusions.",
103
+ "Everything before working_memory leaves future model context unless selected in keep. Put findings from tools, durable decisions, constraints, unresolved matters, and next action in continuation without duplicating retained messages.",
104
104
  ],
105
105
  parameters: workingMemoryParameters,
106
106
  executionMode: "sequential",
@@ -174,14 +174,32 @@ export default function workingMemoryExtension(pi: ExtensionAPI): void {
174
174
  syncBranch(ctx);
175
175
  });
176
176
 
177
- pi.on("before_agent_start", (event, ctx) => {
177
+ pi.on("before_agent_start", (_event, ctx) => {
178
178
  if (!enabled) return undefined;
179
179
  const usage = ctx.getContextUsage();
180
180
  if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
181
- const reminder = Math.floor(Math.max(0, usage.tokens) / interval);
181
+ const tokens = Math.max(0, Math.floor(usage.tokens));
182
+ const reminder = Math.floor(tokens / interval);
182
183
  if (reminder < 1) return undefined;
183
184
  const instruction = instructions[Math.min(reminder, instructions.length) - 1] ?? instructions[0];
184
- return { systemPrompt: `${event.systemPrompt}\n\n${automaticInstruction(instruction)}` };
185
+ return {
186
+ message: {
187
+ customType: NUDGE_TYPE,
188
+ content: automaticInstruction(instruction),
189
+ display: false,
190
+ details: {
191
+ v: 1,
192
+ kind: "automatic",
193
+ tokens,
194
+ boundaryTokens: reminder * interval,
195
+ reminder,
196
+ tier: Math.min(reminder, instructions.length),
197
+ tierCount: instructions.length,
198
+ anchorToolCallId:
199
+ replayWorkingMemoryState(ctx.sessionManager.getBranch(), true).latestAnchorToolCallId ?? null,
200
+ },
201
+ },
202
+ };
185
203
  });
186
204
 
187
205
  pi.on("turn_end", (event, ctx) => {
@@ -1,4 +1,5 @@
1
1
  import { sessionEntryToContextMessages, type ContextEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { isContextProjectionMessage, isLegacyContextMessage } from "../../shared/context-messages.ts";
2
3
  import type { ActiveWorkingMemoryState } from "./state.ts";
3
4
 
4
5
  type ContextMessage = ContextEvent["messages"][number];
@@ -8,23 +9,13 @@ export interface MemoryUnit {
8
9
  label: string;
9
10
  preview: string;
10
11
  order: number;
11
- suborder: number;
12
12
  messages: ContextMessage[];
13
- rowIds: string[];
14
13
  }
15
14
 
16
- const EXCLUDED_CUSTOM_TYPES = new Set(["tau.autoread", "tau.runtime-context", "tau.working-memory.nudge"]);
17
15
  const REFERENCE_CATALOG_TYPE = "tau.working-memory.references";
18
16
 
19
17
  export function buildMemoryCatalog(branch: readonly SessionEntry[]): Map<string, MemoryUnit> {
20
18
  const catalog = new Map<string, MemoryUnit>();
21
- const results = new Map<string, ContextMessage>();
22
- for (const entry of branch) {
23
- for (const message of sessionEntryToContextMessages(entry)) {
24
- if (message.role === "toolResult") results.set(message.toolCallId, message);
25
- }
26
- }
27
-
28
19
  for (let order = 0; order < branch.length; order += 1) {
29
20
  const entry = branch[order];
30
21
  if (!entry) continue;
@@ -34,53 +25,56 @@ export function buildMemoryCatalog(branch: readonly SessionEntry[]): Map<string,
34
25
  const calls = message.content.filter((block) => block.type === "toolCall");
35
26
  const frameworkCall = calls.some((call) => call.name === "working_memory");
36
27
  if (frameworkCall) continue;
37
- const prose = message.content.filter((block) => block.type !== "toolCall");
38
- if (prose.length > 0) {
28
+ const text = message.content.filter((block) => block.type === "text");
29
+ if (text.length > 0) {
39
30
  const ref = `m:${entry.id}`;
40
31
  catalog.set(ref, {
41
32
  ref,
42
33
  label: "assistant",
43
- preview: previewAssistant(prose),
44
- order,
45
- suborder: 0,
46
- messages: [{ ...message, content: prose }],
47
- rowIds: [],
48
- });
49
- }
50
- for (let index = 0; index < calls.length; index += 1) {
51
- const call = calls[index];
52
- if (!call) continue;
53
- const result = results.get(call.id);
54
- if (result?.role !== "toolResult" || result.toolName !== call.name) continue;
55
- const ref = `t:${entry.id}:${index + 1}`;
56
- catalog.set(ref, {
57
- ref,
58
- label: `tool ${call.name}`,
59
- preview: previewTool(call.arguments),
34
+ preview: previewAssistant(text),
60
35
  order,
61
- suborder: index + 1,
62
- messages: [{ ...message, content: [call] }, result],
63
- rowIds: [call.id],
36
+ messages: [{ ...message, content: text }],
64
37
  });
65
38
  }
66
39
  continue;
67
40
  }
68
- if (message.role === "toolResult") continue;
69
- if (message.role === "custom" && EXCLUDED_CUSTOM_TYPES.has(message.customType)) continue;
41
+ if (message.role !== "user") continue;
70
42
  const ref = `m:${entry.id}`;
71
43
  catalog.set(ref, {
72
44
  ref,
73
- label: message.role === "custom" ? message.customType : message.role,
74
- preview: previewMessage(message),
45
+ label: "user",
46
+ preview: previewUser(message),
75
47
  order,
76
- suborder: 0,
77
48
  messages: [message],
78
- rowIds: outlineRowIds(message),
79
49
  });
80
50
  }
81
51
  return catalog;
82
52
  }
83
53
 
54
+ export function collectPrunedRowIds(branch: readonly SessionEntry[], before: number): string[] {
55
+ const rowIds = new Set<string>();
56
+ for (let index = 0; index < before; index += 1) {
57
+ const entry = branch[index];
58
+ if (!entry) continue;
59
+ for (const message of sessionEntryToContextMessages(entry)) {
60
+ if (message.role === "assistant") {
61
+ for (const block of message.content) {
62
+ if (block.type === "toolCall" && block.name !== "working_memory") rowIds.add(block.id);
63
+ }
64
+ }
65
+ if (
66
+ message.role === "custom" &&
67
+ message.customType === "tau.explore.outline" &&
68
+ isRecord(message.details) &&
69
+ typeof message.details.rowId === "string"
70
+ ) {
71
+ rowIds.add(message.details.rowId);
72
+ }
73
+ }
74
+ }
75
+ return [...rowIds];
76
+ }
77
+
84
78
  export function projectWorkingMemory(
85
79
  messages: readonly ContextMessage[],
86
80
  state: ActiveWorkingMemoryState,
@@ -100,7 +94,7 @@ export function projectWorkingMemory(
100
94
  const unit = catalog.get(ref);
101
95
  return unit ? [unit] : [];
102
96
  })
103
- .sort((left, right) => left.order - right.order || left.suborder - right.suborder);
97
+ .sort((left, right) => left.order - right.order);
104
98
  const projected: ContextMessage[] = [];
105
99
  const projectedRefs: MemoryUnit[][] = [];
106
100
  for (const unit of retained) {
@@ -121,24 +115,23 @@ function referenceCurrentMessages(
121
115
  entries: readonly SessionEntry[],
122
116
  catalog: ReadonlyMap<string, MemoryUnit>,
123
117
  ): MemoryUnit[][] {
124
- const generated = entries.flatMap((entry) =>
125
- sessionEntryToContextMessages(entry).map((message) => ({ entry, message })),
118
+ const generated = entries
119
+ .flatMap((entry) => sessionEntryToContextMessages(entry).map((message) => ({ entry, message })))
120
+ .filter(({ message }) => !isLegacyContextMessage(message));
121
+ const sessionMessages = messages.filter(
122
+ (message) => !isContextProjectionMessage(message) && !isLegacyContextMessage(message),
126
123
  );
127
- if (generated.length !== messages.length) return messages.map(() => []);
128
- const toolRefs = new Map<string, MemoryUnit[]>();
129
- for (const unit of catalog.values()) {
130
- const result = unit.messages.find((message) => message.role === "toolResult");
131
- if (result?.role !== "toolResult") continue;
132
- toolRefs.set(result.toolCallId, [...(toolRefs.get(result.toolCallId) ?? []), unit]);
133
- }
134
- return generated.map(({ entry, message }, index) => {
135
- if (message.role === "assistant") {
136
- const unit = catalog.get(`m:${entry.id}`);
137
- return unit && messages[index]?.role === "assistant" ? [unit] : [];
138
- }
139
- if (message.role === "toolResult") return toolRefs.get(message.toolCallId) ?? [];
124
+ if (generated.length !== sessionMessages.length) return messages.map(() => []);
125
+ let generatedIndex = 0;
126
+ return messages.map((currentMessage) => {
127
+ if (isContextProjectionMessage(currentMessage) || isLegacyContextMessage(currentMessage)) return [];
128
+ const current = generated[generatedIndex];
129
+ generatedIndex += 1;
130
+ if (!current) return [];
131
+ const { entry, message } = current;
132
+ if (message.role !== "user" && message.role !== "assistant") return [];
140
133
  const unit = catalog.get(`m:${entry.id}`);
141
- return unit ? [unit] : [];
134
+ return unit && currentMessage.role === message.role ? [unit] : [];
142
135
  });
143
136
  }
144
137
 
@@ -166,7 +159,7 @@ function addReferenceCatalog(
166
159
  role: "custom",
167
160
  customType: REFERENCE_CATALOG_TYPE,
168
161
  content: [...unique.values()]
169
- .sort((left, right) => left.order - right.order || left.suborder - right.suborder)
162
+ .sort((left, right) => left.order - right.order)
170
163
  .map((unit) => `${unit.ref} (${unit.label}): ${unit.preview}`)
171
164
  .join("\n"),
172
165
  display: false,
@@ -206,36 +199,14 @@ function findAnchor(messages: readonly ContextMessage[], id: string): number {
206
199
  function previewAssistant(blocks: ReadonlyArray<{ type: string }>): string {
207
200
  for (const block of blocks) {
208
201
  if (block.type === "text" && "text" in block && typeof block.text === "string") return compact(block.text);
209
- if (block.type === "thinking" && "thinking" in block && typeof block.thinking === "string") {
210
- return compact(block.thinking);
211
- }
212
202
  }
213
203
  return "";
214
204
  }
215
205
 
216
- function previewTool(args: Record<string, unknown>): string {
217
- return compact(JSON.stringify(args));
218
- }
219
-
220
- function previewMessage(message: ContextMessage): string {
221
- if (message.role === "user" || message.role === "custom") {
222
- if (typeof message.content === "string") return compact(message.content);
223
- const text = message.content.find((part) => part.type === "text");
224
- return text?.type === "text" ? compact(text.text) : "";
225
- }
226
- if (message.role === "bashExecution") return compact(`${message.command}: ${message.output}`);
227
- if (message.role === "assistant") return previewAssistant(message.content);
228
- if (message.role === "toolResult") {
229
- const text = message.content.find((part) => part.type === "text");
230
- return text?.type === "text" ? compact(text.text) : "";
231
- }
232
- return compact(message.summary);
233
- }
234
-
235
- function outlineRowIds(message: ContextMessage): string[] {
236
- if (message.role !== "custom" || message.customType !== "tau.explore.outline") return [];
237
- if (!isRecord(message.details) || typeof message.details.rowId !== "string") return [];
238
- return [message.details.rowId];
206
+ function previewUser(message: Extract<ContextMessage, { role: "user" }>): string {
207
+ if (typeof message.content === "string") return compact(message.content);
208
+ const text = message.content.find((part) => part.type === "text");
209
+ return text?.type === "text" ? compact(text.text) : "";
239
210
  }
240
211
 
241
212
  function compact(value: string): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.29.0",
38
+ "@shanepadgett/tau-tui": "0.31.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
41
  "smol-toml": "1.7.0",
@@ -248,31 +248,17 @@
248
248
  },
249
249
  "additionalProperties": false
250
250
  },
251
- "turnBudget": {
251
+ "subagent": {
252
252
  "type": "object",
253
253
  "properties": {
254
- "enabled": {
255
- "type": "boolean",
256
- "default": true,
257
- "description": "Enable turn-budget hints."
258
- },
259
- "turnLimit": {
260
- "type": "integer",
261
- "default": 30,
262
- "minimum": 1,
263
- "description": "Initial soft cap for tool-using turns per user prompt."
264
- },
265
- "nudgeEveryTurns": {
266
- "type": "integer",
267
- "default": 5,
268
- "minimum": 1,
269
- "description": "Tool-using turn interval between turn-budget hints."
270
- },
271
- "softCapIncrement": {
272
- "type": "integer",
273
- "default": 10,
274
- "minimum": 1,
275
- "description": "Turns added when the soft cap is reached."
254
+ "disabled": {
255
+ "type": "array",
256
+ "items": {
257
+ "type": "string",
258
+ "minLength": 1
259
+ },
260
+ "default": [],
261
+ "description": "Agent names unavailable for delegation."
276
262
  }
277
263
  },
278
264
  "additionalProperties": false
@@ -0,0 +1,19 @@
1
+ import type { ContextEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ type ContextMessage = ContextEvent["messages"][number];
4
+
5
+ export const CONTEXT_PROJECTION_TYPE = "tau.context.projection";
6
+
7
+ export function isContextProjectionMessage(message: ContextMessage): boolean {
8
+ return message.role === "custom" && message.customType === CONTEXT_PROJECTION_TYPE;
9
+ }
10
+
11
+ export function isLegacyContextMessage(message: ContextMessage): boolean {
12
+ if (message.role !== "custom") return false;
13
+ if (message.customType !== "tau.injected-context" && message.customType !== "tau.autoread") return false;
14
+ return (
15
+ message.details !== undefined &&
16
+ typeof message.details === "object" &&
17
+ (message.details as Record<string, unknown>).source === "context"
18
+ );
19
+ }
@@ -3,7 +3,7 @@ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
  export const INJECTED_CONTEXT_TYPE = "tau.injected-context";
4
4
 
5
5
  export interface InjectedContextDetails {
6
- source: "review" | "context";
6
+ source: "review";
7
7
  title?: string;
8
8
  }
9
9
 
@@ -61,7 +61,7 @@ export function getBranchInjectedContexts(entries: readonly SessionEntry[]): Inj
61
61
  function readDetails(value: unknown): InjectedContextDetails | undefined {
62
62
  if (!value || typeof value !== "object") return undefined;
63
63
  const record = value as Record<string, unknown>;
64
- if (record.source !== "review" && record.source !== "context") return undefined;
64
+ if (record.source !== "review") return undefined;
65
65
  return {
66
66
  source: record.source,
67
67
  ...(typeof record.title === "string" ? { title: record.title } : {}),
@@ -0,0 +1,151 @@
1
+ import {
2
+ createAgentSession,
3
+ DefaultResourceLoader,
4
+ getAgentDir,
5
+ ModelRuntime,
6
+ SessionManager,
7
+ type AgentSession,
8
+ type ExtensionContext,
9
+ type ToolDefinition,
10
+ } from "@earendil-works/pi-coding-agent";
11
+
12
+ type IsolatedSessionThinkingLevel = NonNullable<ExtensionContext["thinkingLevel"]>;
13
+
14
+ type SelectedModel = NonNullable<ExtensionContext["model"]>;
15
+ type SelectedProvider = NonNullable<ReturnType<ExtensionContext["modelRegistry"]["getProvider"]>>;
16
+
17
+ export interface IsolatedSessionInputs {
18
+ label: string;
19
+ extensionPaths: readonly string[];
20
+ cwd: string;
21
+ model: SelectedModel;
22
+ provider: SelectedProvider;
23
+ runtimeApiKey: string | undefined;
24
+ thinkingLevel: IsolatedSessionThinkingLevel;
25
+ tools: readonly string[];
26
+ customTools: readonly ToolDefinition[];
27
+ bindTarget: { mode: "print" } | { mode: "tui"; uiContext: ExtensionContext["ui"] };
28
+ }
29
+
30
+ export interface IsolatedSessionResource {
31
+ readonly session: AgentSession;
32
+ dispose(): Promise<void>;
33
+ }
34
+
35
+ export async function resolveIsolatedSessionModel(options: {
36
+ label: string;
37
+ preferredModel: string | undefined;
38
+ preferredThinkingLevel: IsolatedSessionThinkingLevel | undefined;
39
+ usePreferredThinkingAfterModelFallback: boolean;
40
+ ctx: ExtensionContext;
41
+ parentThinkingLevel: IsolatedSessionThinkingLevel;
42
+ signal: AbortSignal;
43
+ onWarning?: (warning: string) => void;
44
+ }): Promise<Pick<IsolatedSessionInputs, "model" | "provider" | "runtimeApiKey" | "thinkingLevel">> {
45
+ const { ctx, signal, onWarning } = options;
46
+ let model = ctx.model;
47
+ let thinkingLevel = options.parentThinkingLevel;
48
+ let preferredModelSelected = false;
49
+ let selectedAuth: Awaited<ReturnType<ExtensionContext["modelRegistry"]["getApiKeyAndHeaders"]>> | undefined;
50
+ if (options.preferredModel) {
51
+ const separator = options.preferredModel.indexOf("/");
52
+ const configured = ctx.modelRegistry.find(
53
+ options.preferredModel.slice(0, separator),
54
+ options.preferredModel.slice(separator + 1),
55
+ );
56
+ if (!configured) onWarning?.(`model ${options.preferredModel} is unavailable; using parent model`);
57
+ else {
58
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
59
+ if (!auth.ok) onWarning?.(`model ${options.preferredModel} is unavailable: ${auth.error}; using parent model`);
60
+ else {
61
+ model = configured;
62
+ selectedAuth = auth;
63
+ preferredModelSelected = true;
64
+ }
65
+ }
66
+ }
67
+ if (options.preferredThinkingLevel && (preferredModelSelected || options.usePreferredThinkingAfterModelFallback)) {
68
+ const preferred = options.preferredThinkingLevel;
69
+ const mapped = model?.thinkingLevelMap?.[preferred];
70
+ const unsupported =
71
+ !model?.reasoning ||
72
+ mapped === null ||
73
+ ((preferred === "xhigh" || preferred === "max") && mapped === undefined);
74
+ if (unsupported)
75
+ onWarning?.(`thinking ${preferred} is unavailable for the selected model; using parent thinking`);
76
+ else thinkingLevel = preferred;
77
+ }
78
+ if (!model) throw new Error(`${options.label} startup failed: parent has no model`);
79
+ const auth = selectedAuth ?? (await ctx.modelRegistry.getApiKeyAndHeaders(model));
80
+ if (!auth.ok) throw new Error(`${options.label} startup failed: ${auth.error}`);
81
+ const provider = ctx.modelRegistry.getProvider(model.provider);
82
+ if (!provider) throw new Error(`${options.label} startup failed: provider ${model.provider} is unavailable`);
83
+ if (signal.aborted) throw new Error(`${options.label} startup aborted`);
84
+ return {
85
+ model,
86
+ provider,
87
+ runtimeApiKey:
88
+ auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model) ? auth.apiKey : undefined,
89
+ thinkingLevel,
90
+ };
91
+ }
92
+
93
+ export async function createIsolatedSessionResource(
94
+ inputs: IsolatedSessionInputs,
95
+ signal: AbortSignal,
96
+ ): Promise<IsolatedSessionResource> {
97
+ let session: AgentSession | undefined;
98
+ try {
99
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
100
+ const modelRuntime = await ModelRuntime.create();
101
+ modelRuntime.registerNativeProvider(inputs.provider);
102
+ if (inputs.runtimeApiKey !== undefined)
103
+ await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
104
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
105
+ const resourceLoader = new DefaultResourceLoader({
106
+ cwd: inputs.cwd,
107
+ agentDir: getAgentDir(),
108
+ noExtensions: true,
109
+ additionalExtensionPaths: [...inputs.extensionPaths],
110
+ });
111
+ await resourceLoader.reload();
112
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
113
+ const created = await createAgentSession({
114
+ cwd: inputs.cwd,
115
+ model: inputs.model,
116
+ modelRuntime,
117
+ thinkingLevel: inputs.thinkingLevel,
118
+ tools: [...inputs.tools],
119
+ excludeTools: ["subagent"],
120
+ resourceLoader,
121
+ customTools: [...inputs.customTools],
122
+ sessionManager: SessionManager.inMemory(inputs.cwd),
123
+ });
124
+ session = created.session;
125
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
126
+ await session.bindExtensions(inputs.bindTarget);
127
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
128
+ const active = session.getActiveToolNames().sort();
129
+ const expected = [...inputs.tools].sort();
130
+ if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
131
+ const missing = expected.filter((tool) => !active.includes(tool));
132
+ throw new Error(
133
+ `${inputs.label} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
134
+ );
135
+ }
136
+ let disposed = false;
137
+ return {
138
+ session,
139
+ async dispose() {
140
+ if (disposed) return;
141
+ disposed = true;
142
+ if (session?.isStreaming) await session.abort().catch(() => undefined);
143
+ session?.dispose();
144
+ },
145
+ };
146
+ } catch (error) {
147
+ if (session?.isStreaming) await session.abort().catch(() => undefined);
148
+ session?.dispose();
149
+ throw error;
150
+ }
151
+ }